sidekiq 7.3.9 → 8.1.7
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 +225 -0
- data/README.md +34 -14
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/sidekiqload +12 -10
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +107 -58
- data/lib/generators/sidekiq/job_generator.rb +15 -3
- data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
- data/lib/sidekiq/api.rb +310 -83
- data/lib/sidekiq/capsule.rb +10 -7
- data/lib/sidekiq/cli.rb +31 -24
- data/lib/sidekiq/client.rb +36 -18
- data/lib/sidekiq/component.rb +45 -3
- data/lib/sidekiq/config.rb +35 -30
- data/lib/sidekiq/embedded.rb +2 -1
- data/lib/sidekiq/fetch.rb +1 -0
- data/lib/sidekiq/iterable_job.rb +1 -0
- data/lib/sidekiq/job/iterable.rb +46 -18
- data/lib/sidekiq/job.rb +18 -3
- data/lib/sidekiq/job_logger.rb +8 -6
- data/lib/sidekiq/job_retry.rb +45 -18
- data/lib/sidekiq/job_util.rb +5 -1
- data/lib/sidekiq/launcher.rb +22 -12
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +25 -69
- data/lib/sidekiq/manager.rb +2 -2
- data/lib/sidekiq/metrics/query.rb +77 -46
- data/lib/sidekiq/metrics/shared.rb +8 -5
- data/lib/sidekiq/metrics/tracking.rb +12 -7
- data/lib/sidekiq/middleware/current_attributes.rb +11 -19
- data/lib/sidekiq/middleware/i18n.rb +2 -0
- data/lib/sidekiq/monitor.rb +4 -8
- data/lib/sidekiq/paginator.rb +18 -2
- data/lib/sidekiq/processor.rb +26 -18
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +46 -67
- data/lib/sidekiq/redis_client_adapter.rb +6 -3
- data/lib/sidekiq/redis_connection.rb +16 -5
- data/lib/sidekiq/ring_buffer.rb +1 -0
- data/lib/sidekiq/scheduled.rb +6 -7
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -333
- data/lib/sidekiq/transaction_aware_client.rb +13 -5
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
- data/lib/sidekiq/tui/tabs/busy.rb +104 -0
- data/lib/sidekiq/tui/tabs/dead.rb +14 -0
- data/lib/sidekiq/tui/tabs/home.rb +115 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
- data/lib/sidekiq/tui/tabs/queues.rb +83 -0
- data/lib/sidekiq/tui/tabs/retries.rb +14 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +390 -0
- data/lib/sidekiq/version.rb +2 -2
- data/lib/sidekiq/web/action.rb +146 -83
- data/lib/sidekiq/web/application.rb +372 -328
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +102 -44
- data/lib/sidekiq/web/router.rb +61 -77
- data/lib/sidekiq/web.rb +65 -151
- data/lib/sidekiq.rb +13 -1
- data/sidekiq.gemspec +9 -8
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +43 -27
- 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.js +1 -1
- data/web/assets/javascripts/metrics.js +16 -34
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +3 -1
- 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 +8 -0
- data/web/locales/es.yml +25 -2
- data/web/locales/fa.yml +3 -1
- data/web/locales/fr.yml +3 -1
- data/web/locales/gd.yml +13 -1
- data/web/locales/he.yml +3 -1
- data/web/locales/hi.yml +36 -0
- data/web/locales/it.yml +9 -0
- data/web/locales/ja.yml +2 -0
- 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} +3 -1
- 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 +83 -47
- data/web/locales/tr.yml +2 -0
- data/web/locales/uk.yml +7 -5
- data/web/locales/ur.yml +3 -1
- data/web/locales/vi.yml +2 -0
- data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
- data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
- data/web/views/_footer.html.erb +32 -0
- data/web/views/_job_info.html.erb +115 -0
- data/web/views/_metrics_period_select.html.erb +15 -0
- data/web/views/_nav.html.erb +45 -0
- data/web/views/_paging.html.erb +26 -0
- data/web/views/_poll_link.html.erb +4 -0
- data/web/views/{_summary.erb → _summary.html.erb} +16 -16
- data/web/views/busy.html.erb +155 -0
- data/web/views/dashboard.html.erb +104 -0
- data/web/views/dead.html.erb +38 -0
- data/web/views/filtering.html.erb +6 -0
- data/web/views/layout.html.erb +26 -0
- data/web/views/metrics.html.erb +85 -0
- data/web/views/metrics_for_job.html.erb +58 -0
- data/web/views/morgue.html.erb +69 -0
- data/web/views/profiles.html.erb +43 -0
- data/web/views/queue.html.erb +57 -0
- data/web/views/queues.html.erb +46 -0
- data/web/views/retries.html.erb +77 -0
- data/web/views/retry.html.erb +39 -0
- data/web/views/scheduled.html.erb +64 -0
- data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
- metadata +65 -47
- data/lib/sidekiq/web/csrf_protection.rb +0 -183
- 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/_footer.erb +0 -34
- data/web/views/_job_info.erb +0 -105
- data/web/views/_metrics_period_select.erb +0 -12
- data/web/views/_nav.erb +0 -52
- data/web/views/_paging.erb +0 -25
- data/web/views/_poll_link.erb +0 -4
- data/web/views/_status.erb +0 -4
- data/web/views/busy.erb +0 -148
- data/web/views/dashboard.erb +0 -108
- data/web/views/dead.erb +0 -34
- data/web/views/filtering.erb +0 -6
- data/web/views/layout.erb +0 -42
- data/web/views/metrics.erb +0 -90
- data/web/views/metrics_for_job.erb +0 -59
- data/web/views/morgue.erb +0 -74
- data/web/views/queue.erb +0 -55
- data/web/views/queues.erb +0 -44
- data/web/views/retries.erb +0 -79
- data/web/views/retry.erb +0 -34
- data/web/views/scheduled.erb +0 -56
|
@@ -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,29 @@ 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
|
+
days = (time_range.begin.utc.to_date..time_range.end.utc.to_date)
|
|
173
|
+
marks = @pool.with { |c|
|
|
174
|
+
c.pipelined { |pipeline|
|
|
175
|
+
days.each { |day| pipeline.hgetall("#{day.strftime("%Y%m%d")}-marks") }
|
|
176
|
+
}
|
|
177
|
+
}.flat_map(&:to_a)
|
|
147
178
|
|
|
148
179
|
marks.each do |timestamp, label|
|
|
149
180
|
time = Time.parse(timestamp)
|
|
150
181
|
if time_range.cover? time
|
|
151
|
-
result << MarkResult.new(time, label)
|
|
182
|
+
result << MarkResult.new(time, label, Query.bkt_time_s(time, granularity))
|
|
152
183
|
end
|
|
153
184
|
end
|
|
154
185
|
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
|
|
|
@@ -82,15 +85,15 @@ module Sidekiq
|
|
|
82
85
|
end
|
|
83
86
|
|
|
84
87
|
def fetch(conn, now = Time.now)
|
|
85
|
-
window = now.utc.strftime("
|
|
86
|
-
key = "
|
|
88
|
+
window = now.utc.strftime("%-d-%-H:%-M")
|
|
89
|
+
key = "h|#{@klass}-#{window}"
|
|
87
90
|
conn.bitfield_ro(key, *FETCH)
|
|
88
91
|
end
|
|
89
92
|
|
|
90
93
|
def persist(conn, now = Time.now)
|
|
91
94
|
buckets, @buckets = @buckets, []
|
|
92
|
-
window = now.utc.strftime("
|
|
93
|
-
key = "
|
|
95
|
+
window = now.utc.strftime("%-d-%-H:%-M")
|
|
96
|
+
key = "h|#{@klass}-#{window}"
|
|
94
97
|
cmd = [key, "OVERFLOW", "SAT"]
|
|
95
98
|
buckets.each_with_index do |counter, idx|
|
|
96
99
|
val = counter.value
|
|
@@ -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}"
|
|
@@ -145,4 +147,7 @@ Sidekiq.configure_server do |config|
|
|
|
145
147
|
config.on(:beat) do
|
|
146
148
|
exec.flush
|
|
147
149
|
end
|
|
150
|
+
config.on(:exit) do
|
|
151
|
+
exec.flush
|
|
152
|
+
end
|
|
148
153
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_job/arguments"
|
|
3
4
|
require "active_support/current_attributes"
|
|
4
5
|
|
|
5
6
|
module Sidekiq
|
|
@@ -20,6 +21,8 @@ module Sidekiq
|
|
|
20
21
|
# Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
|
|
21
22
|
#
|
|
22
23
|
module CurrentAttributes
|
|
24
|
+
Serializer = ::ActiveJob::Arguments
|
|
25
|
+
|
|
23
26
|
class Save
|
|
24
27
|
include Sidekiq::ClientMiddleware
|
|
25
28
|
|
|
@@ -33,26 +36,11 @@ module Sidekiq
|
|
|
33
36
|
attrs = strklass.constantize.attributes
|
|
34
37
|
# Retries can push the job N times, we don't
|
|
35
38
|
# want retries to reset cattr. #5692, #5090
|
|
36
|
-
if attrs.any?
|
|
37
|
-
# Older rails has a bug that `CurrentAttributes#attributes` always returns
|
|
38
|
-
# the same hash instance. We need to dup it to avoid being accidentally mutated.
|
|
39
|
-
job[key] = if returns_same_object?
|
|
40
|
-
attrs.dup
|
|
41
|
-
else
|
|
42
|
-
attrs
|
|
43
|
-
end
|
|
44
|
-
end
|
|
39
|
+
job[key] = Serializer.serialize(attrs) if attrs.any?
|
|
45
40
|
end
|
|
46
41
|
end
|
|
47
42
|
yield
|
|
48
43
|
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def returns_same_object?
|
|
53
|
-
ActiveSupport::VERSION::MAJOR < 8 ||
|
|
54
|
-
(ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
|
|
55
|
-
end
|
|
56
44
|
end
|
|
57
45
|
|
|
58
46
|
class Load
|
|
@@ -62,13 +50,13 @@ module Sidekiq
|
|
|
62
50
|
@cattrs = cattrs
|
|
63
51
|
end
|
|
64
52
|
|
|
65
|
-
def call(_, job,
|
|
53
|
+
def call(_, job, *, &block)
|
|
66
54
|
klass_attrs = {}
|
|
67
55
|
|
|
68
56
|
@cattrs.each do |(key, strklass)|
|
|
69
57
|
next unless job.has_key?(key)
|
|
70
58
|
|
|
71
|
-
klass_attrs[strklass.constantize] = job[key]
|
|
59
|
+
klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
|
|
72
60
|
end
|
|
73
61
|
|
|
74
62
|
wrap(klass_attrs.to_a, &block)
|
|
@@ -83,11 +71,14 @@ module Sidekiq
|
|
|
83
71
|
retried = false
|
|
84
72
|
|
|
85
73
|
begin
|
|
74
|
+
set_succeeded = false
|
|
86
75
|
klass.set(attrs) do
|
|
76
|
+
set_succeeded = true
|
|
87
77
|
wrap(klass_attrs, &block)
|
|
88
78
|
end
|
|
89
79
|
rescue NoMethodError
|
|
90
|
-
|
|
80
|
+
# Don't retry if the no method error didn't come from current attributes
|
|
81
|
+
raise if retried || set_succeeded
|
|
91
82
|
|
|
92
83
|
# It is possible that the `CurrentAttributes` definition
|
|
93
84
|
# was changed before the job started processing.
|
|
@@ -102,6 +93,7 @@ module Sidekiq
|
|
|
102
93
|
def persist(klass_or_array, config = Sidekiq.default_configuration)
|
|
103
94
|
cattrs = build_cattrs_hash(klass_or_array)
|
|
104
95
|
|
|
96
|
+
config.client_middleware.prepend Load, cattrs
|
|
105
97
|
config.client_middleware.add Save, cattrs
|
|
106
98
|
config.server_middleware.prepend Load, cattrs
|
|
107
99
|
end
|
|
@@ -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
|
@@ -49,14 +49,10 @@ class Sidekiq::Monitor
|
|
|
49
49
|
puts "---- Processes (#{process_set.size}) ----"
|
|
50
50
|
process_set.each_with_index do |process, index|
|
|
51
51
|
# Keep compatibility with legacy versions since we don't want to break sidekiqmon during rolling upgrades or downgrades.
|
|
52
|
-
#
|
|
53
|
-
# Before:
|
|
54
|
-
# ["default", "critical"]
|
|
55
|
-
#
|
|
56
|
-
# After:
|
|
57
|
-
# {"default" => 1, "critical" => 10}
|
|
58
52
|
queues =
|
|
59
|
-
if process["
|
|
53
|
+
if process["capsules"] # 8.0.6+
|
|
54
|
+
process["capsules"].values.map { |x| x["weights"].keys.join(", ") }
|
|
55
|
+
elsif process["weights"]
|
|
60
56
|
process["weights"].sort_by { |queue| queue[0] }.map { |capsule| capsule.map { |name, weight| (weight > 0) ? "#{name}: #{weight}" : name }.join(", ") }
|
|
61
57
|
else
|
|
62
58
|
process["queues"].sort
|
|
@@ -105,7 +101,7 @@ class Sidekiq::Monitor
|
|
|
105
101
|
out << line
|
|
106
102
|
line = " " * pad
|
|
107
103
|
end
|
|
108
|
-
line << value + "
|
|
104
|
+
line << value + "; "
|
|
109
105
|
end
|
|
110
106
|
out << line[0..-3]
|
|
111
107
|
out.join("\n")
|
data/lib/sidekiq/paginator.rb
CHANGED
|
@@ -17,7 +17,15 @@ module Sidekiq
|
|
|
17
17
|
ending = starting + page_size - 1
|
|
18
18
|
|
|
19
19
|
Sidekiq.redis do |conn|
|
|
20
|
-
|
|
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 = conn.type(key)
|
|
27
|
+
TYPE_CACHE[key] = type unless type == "none"
|
|
28
|
+
end
|
|
21
29
|
rev = opts && opts[:reverse]
|
|
22
30
|
|
|
23
31
|
case type
|
|
@@ -52,10 +60,18 @@ module Sidekiq
|
|
|
52
60
|
|
|
53
61
|
def page_items(items, pageidx = 1, page_size = 25)
|
|
54
62
|
current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
|
|
63
|
+
# A negative page_size makes Array#[] return nil instead of a slice, which
|
|
64
|
+
# would crash callers iterating the result (e.g. the Web UI Busy page).
|
|
65
|
+
page_size = 0 if page_size < 0
|
|
55
66
|
pageidx = current_page - 1
|
|
56
67
|
starting = pageidx * page_size
|
|
57
68
|
items = items.to_a
|
|
58
|
-
|
|
69
|
+
total_size = items.size
|
|
70
|
+
if starting > total_size
|
|
71
|
+
starting = 0
|
|
72
|
+
current_page = 1
|
|
73
|
+
end
|
|
74
|
+
[current_page, total_size, items[starting, page_size]]
|
|
59
75
|
end
|
|
60
76
|
end
|
|
61
77
|
end
|
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
|
##
|
|
@@ -29,15 +30,16 @@ module Sidekiq
|
|
|
29
30
|
attr_reader :capsule
|
|
30
31
|
|
|
31
32
|
def initialize(capsule, &block)
|
|
32
|
-
@config =
|
|
33
|
+
@config = capsule.config
|
|
34
|
+
@capsule = capsule
|
|
33
35
|
@callback = block
|
|
34
36
|
@down = false
|
|
35
37
|
@done = false
|
|
36
38
|
@job = nil
|
|
37
39
|
@thread = nil
|
|
38
40
|
@reloader = Sidekiq.default_configuration[:reloader]
|
|
39
|
-
@job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(
|
|
40
|
-
@retrier = Sidekiq::JobRetry.new(capsule)
|
|
41
|
+
@job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(@config)
|
|
42
|
+
@retrier = Sidekiq::JobRetry.new(@capsule)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def terminate(wait = false)
|
|
@@ -63,10 +65,10 @@ module Sidekiq
|
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
def start
|
|
66
|
-
@thread ||= safe_thread("#{
|
|
68
|
+
@thread ||= safe_thread("#{capsule.name}/processor", &method(:run))
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
private
|
|
71
|
+
private
|
|
70
72
|
|
|
71
73
|
def run
|
|
72
74
|
# By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
|
|
@@ -112,13 +114,17 @@ module Sidekiq
|
|
|
112
114
|
def handle_fetch_exception(ex)
|
|
113
115
|
unless @down
|
|
114
116
|
@down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
115
|
-
logger.error("Error fetching job: #{ex}")
|
|
116
117
|
handle_exception(ex)
|
|
117
118
|
end
|
|
118
119
|
sleep(1)
|
|
119
120
|
nil
|
|
120
121
|
end
|
|
121
122
|
|
|
123
|
+
def profile(job, &block)
|
|
124
|
+
return yield unless job["profile"]
|
|
125
|
+
Sidekiq::Profiler.new(config).call(job, &block)
|
|
126
|
+
end
|
|
127
|
+
|
|
122
128
|
def dispatch(job_hash, queue, jobstr)
|
|
123
129
|
# since middleware can mutate the job hash
|
|
124
130
|
# we need to clone it to report the original
|
|
@@ -132,17 +138,19 @@ module Sidekiq
|
|
|
132
138
|
@retrier.global(jobstr, queue) do
|
|
133
139
|
@job_logger.call(job_hash, queue) do
|
|
134
140
|
stats(jobstr, queue) do
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
profile(job_hash) do
|
|
142
|
+
# Rails 5 requires a Reloader to wrap code execution. In order to
|
|
143
|
+
# constantize the worker and instantiate an instance, we have to call
|
|
144
|
+
# the Reloader. It handles code loading, db connection management, etc.
|
|
145
|
+
# Effectively this block denotes a "unit of work" to Rails.
|
|
146
|
+
@reloader.call do
|
|
147
|
+
klass = Object.const_get(job_hash["class"])
|
|
148
|
+
instance = klass.new
|
|
149
|
+
instance.jid = job_hash["jid"]
|
|
150
|
+
instance._context = self
|
|
151
|
+
@retrier.local(instance, jobstr, queue) do
|
|
152
|
+
yield instance
|
|
153
|
+
end
|
|
146
154
|
end
|
|
147
155
|
end
|
|
148
156
|
end
|
|
@@ -165,7 +173,6 @@ module Sidekiq
|
|
|
165
173
|
begin
|
|
166
174
|
job_hash = Sidekiq.load_json(jobstr)
|
|
167
175
|
rescue => ex
|
|
168
|
-
handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
|
|
169
176
|
now = Time.now.to_f
|
|
170
177
|
redis do |conn|
|
|
171
178
|
conn.multi do |xa|
|
|
@@ -174,6 +181,7 @@ module Sidekiq
|
|
|
174
181
|
xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
|
|
175
182
|
end
|
|
176
183
|
end
|
|
184
|
+
handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
|
|
177
185
|
return uow.acknowledge
|
|
178
186
|
end
|
|
179
187
|
|
|
@@ -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["wrapped"] || 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
|