sidekiq 6.5.10 → 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 +568 -18
- data/README.md +66 -38
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +3 -8
- data/bin/sidekiqload +215 -118
- data/bin/sidekiqmon +3 -0
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/lib/generators/sidekiq/job_generator.rb +17 -3
- data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
- data/lib/sidekiq/api.rb +526 -218
- data/lib/sidekiq/capsule.rb +135 -0
- data/lib/sidekiq/cli.rb +88 -96
- data/lib/sidekiq/client.rb +114 -47
- data/lib/sidekiq/component.rb +71 -4
- data/lib/sidekiq/config.rb +316 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +64 -0
- data/lib/sidekiq/fetch.rb +12 -14
- 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 +397 -10
- data/lib/sidekiq/job_logger.rb +14 -11
- data/lib/sidekiq/job_retry.rb +78 -28
- data/lib/sidekiq/job_util.rb +58 -16
- data/lib/sidekiq/launcher.rb +86 -70
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +25 -94
- data/lib/sidekiq/manager.rb +11 -13
- data/lib/sidekiq/metrics/query.rb +84 -48
- data/lib/sidekiq/metrics/shared.rb +29 -14
- data/lib/sidekiq/metrics/tracking.rb +49 -30
- data/lib/sidekiq/middleware/chain.rb +19 -18
- data/lib/sidekiq/middleware/current_attributes.rb +78 -21
- data/lib/sidekiq/middleware/i18n.rb +2 -0
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +15 -5
- data/lib/sidekiq/paginator.rb +26 -4
- data/lib/sidekiq/processor.rb +78 -65
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +45 -60
- data/lib/sidekiq/redis_client_adapter.rb +35 -72
- data/lib/sidekiq/redis_connection.rb +54 -114
- data/lib/sidekiq/ring_buffer.rb +3 -0
- data/lib/sidekiq/scheduled.rb +28 -30
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -342
- data/lib/sidekiq/transaction_aware_client.rb +23 -9
- 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 +6 -1
- data/lib/sidekiq/web/action.rb +149 -64
- data/lib/sidekiq/web/application.rb +379 -281
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +190 -71
- data/lib/sidekiq/web/router.rb +62 -75
- data/lib/sidekiq/web.rb +68 -101
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +85 -269
- data/sidekiq.gemspec +15 -11
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +84 -24
- data/web/assets/javascripts/base-charts.js +120 -0
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +194 -0
- data/web/assets/javascripts/dashboard.js +18 -234
- data/web/assets/javascripts/metrics.js +138 -120
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +72 -70
- data/web/locales/cs.yml +64 -62
- data/web/locales/da.yml +62 -53
- data/web/locales/de.yml +67 -65
- data/web/locales/el.yml +4 -7
- data/web/locales/en.yml +89 -71
- data/web/locales/es.yml +91 -68
- data/web/locales/fa.yml +67 -65
- data/web/locales/fr.yml +82 -67
- data/web/locales/gd.yml +110 -0
- data/web/locales/he.yml +67 -64
- data/web/locales/hi.yml +95 -59
- data/web/locales/it.yml +94 -54
- data/web/locales/ja.yml +69 -70
- data/web/locales/ko.yml +54 -52
- data/web/locales/lt.yml +68 -66
- data/web/locales/nb.yml +63 -61
- data/web/locales/nl.yml +54 -52
- data/web/locales/pl.yml +47 -45
- data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
- data/web/locales/pt.yml +53 -51
- data/web/locales/ru.yml +69 -66
- data/web/locales/sv.yml +55 -53
- data/web/locales/ta.yml +100 -64
- data/web/locales/tr.yml +102 -0
- data/web/locales/uk.yml +87 -61
- data/web/locales/ur.yml +66 -64
- data/web/locales/vi.yml +69 -67
- data/web/locales/zh-CN.yml +107 -0
- data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
- 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.html.erb +40 -0
- 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 +113 -70
- 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/metrics/deploy.rb +0 -47
- data/lib/sidekiq/web/csrf_protection.rb +0 -180
- data/lib/sidekiq/worker.rb +0 -370
- data/web/assets/javascripts/graph.js +0 -16
- data/web/assets/stylesheets/application-dark.css +0 -143
- data/web/assets/stylesheets/application-rtl.css +0 -242
- data/web/assets/stylesheets/application.css +0 -999
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/locales/zh-cn.yml +0 -93
- data/web/views/_footer.erb +0 -20
- data/web/views/_job_info.erb +0 -89
- data/web/views/_nav.erb +0 -52
- data/web/views/_paging.erb +0 -23
- data/web/views/_poll_link.erb +0 -4
- data/web/views/_status.erb +0 -4
- data/web/views/_summary.erb +0 -40
- data/web/views/busy.erb +0 -137
- data/web/views/dashboard.erb +0 -84
- data/web/views/dead.erb +0 -34
- data/web/views/layout.erb +0 -42
- data/web/views/metrics.erb +0 -69
- data/web/views/metrics_for_job.erb +0 -87
- data/web/views/morgue.erb +0 -78
- data/web/views/queue.erb +0 -59
- data/web/views/queues.erb +0 -38
- data/web/views/retries.erb +0 -83
- data/web/views/retry.erb +0 -34
- data/web/views/scheduled.erb +0 -57
- /data/{LICENSE → LICENSE.txt} +0 -0
|
@@ -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,94 +10,122 @@ 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
|
-
def initialize(pool:
|
|
16
|
+
def initialize(pool: nil, now: Time.now)
|
|
17
17
|
@time = now.utc
|
|
18
|
-
@pool = pool
|
|
18
|
+
@pool = pool || Sidekiq.default_configuration.redis_pool
|
|
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)
|
|
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,29 @@ 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
|
+
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)
|
|
142
178
|
|
|
143
179
|
marks.each do |timestamp, label|
|
|
144
180
|
time = Time.parse(timestamp)
|
|
145
181
|
if time_range.cover? time
|
|
146
|
-
result << MarkResult.new(time, label)
|
|
182
|
+
result << MarkResult.new(time, label, Query.bkt_time_s(time, granularity))
|
|
147
183
|
end
|
|
148
184
|
end
|
|
149
185
|
end
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Sidekiq
|
|
4
4
|
module Metrics
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
7
19
|
|
|
8
20
|
# Implements space-efficient but statistically useful histogram storage.
|
|
9
21
|
# A precise time histogram stores every time. Instead we break times into a set of
|
|
@@ -13,7 +25,10 @@ module Sidekiq
|
|
|
13
25
|
#
|
|
14
26
|
# To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
|
|
15
27
|
# per bucket per klass per minute. It's unlikely that most people will be executing more
|
|
16
|
-
# 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.
|
|
17
32
|
class Histogram
|
|
18
33
|
include Enumerable
|
|
19
34
|
|
|
@@ -28,8 +43,8 @@ module Sidekiq
|
|
|
28
43
|
1100, 1700, 2500, 3800, 5750,
|
|
29
44
|
8500, 13000, 20000, 30000, 45000,
|
|
30
45
|
65000, 100000, 150000, 225000, 335000,
|
|
31
|
-
|
|
32
|
-
]
|
|
46
|
+
1e20 # the "maybe your job is too long" bucket
|
|
47
|
+
].freeze
|
|
33
48
|
LABELS = [
|
|
34
49
|
"20ms", "30ms", "45ms", "65ms", "100ms",
|
|
35
50
|
"150ms", "225ms", "335ms", "500ms", "750ms",
|
|
@@ -37,8 +52,7 @@ module Sidekiq
|
|
|
37
52
|
"8.5s", "13s", "20s", "30s", "45s",
|
|
38
53
|
"65s", "100s", "150s", "225s", "335s",
|
|
39
54
|
"Slow"
|
|
40
|
-
]
|
|
41
|
-
|
|
55
|
+
].freeze
|
|
42
56
|
FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
|
|
43
57
|
GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
|
|
44
58
|
GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
|
|
@@ -46,6 +60,7 @@ module Sidekiq
|
|
|
46
60
|
GET u16 #16 GET u16 #17 GET u16 #18 GET u16 #19 \
|
|
47
61
|
GET u16 #20 GET u16 #21 GET u16 #22 GET u16 #23 \
|
|
48
62
|
GET u16 #24 GET u16 #25".split
|
|
63
|
+
HISTOGRAM_TTL = 8 * 60 * 60
|
|
49
64
|
|
|
50
65
|
def each
|
|
51
66
|
buckets.each { |counter| yield counter.value }
|
|
@@ -70,15 +85,15 @@ module Sidekiq
|
|
|
70
85
|
end
|
|
71
86
|
|
|
72
87
|
def fetch(conn, now = Time.now)
|
|
73
|
-
window = now.utc.strftime("
|
|
74
|
-
key = "
|
|
75
|
-
conn.
|
|
88
|
+
window = now.utc.strftime("%-d-%-H:%-M")
|
|
89
|
+
key = "h|#{@klass}-#{window}"
|
|
90
|
+
conn.bitfield_ro(key, *FETCH)
|
|
76
91
|
end
|
|
77
92
|
|
|
78
93
|
def persist(conn, now = Time.now)
|
|
79
94
|
buckets, @buckets = @buckets, []
|
|
80
|
-
window = now.utc.strftime("
|
|
81
|
-
key = "
|
|
95
|
+
window = now.utc.strftime("%-d-%-H:%-M")
|
|
96
|
+
key = "h|#{@klass}-#{window}"
|
|
82
97
|
cmd = [key, "OVERFLOW", "SAT"]
|
|
83
98
|
buckets.each_with_index do |counter, idx|
|
|
84
99
|
val = counter.value
|
|
@@ -86,7 +101,7 @@ module Sidekiq
|
|
|
86
101
|
end
|
|
87
102
|
|
|
88
103
|
conn.bitfield(*cmd) if cmd.size > 3
|
|
89
|
-
conn.expire(key,
|
|
104
|
+
conn.expire(key, HISTOGRAM_TTL)
|
|
90
105
|
key
|
|
91
106
|
end
|
|
92
107
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "time"
|
|
2
4
|
require "sidekiq"
|
|
3
5
|
require "sidekiq/metrics/shared"
|
|
@@ -17,23 +19,23 @@ module Sidekiq
|
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def track(queue, klass)
|
|
20
|
-
start =
|
|
22
|
+
start = mono_ms
|
|
21
23
|
time_ms = 0
|
|
22
24
|
begin
|
|
23
25
|
begin
|
|
24
26
|
yield
|
|
25
27
|
ensure
|
|
26
|
-
finish =
|
|
28
|
+
finish = mono_ms
|
|
27
29
|
time_ms = finish - start
|
|
28
30
|
end
|
|
29
31
|
# We don't track time for failed jobs as they can have very unpredictable
|
|
30
32
|
# execution times. more important to know average time for successful jobs so we
|
|
31
33
|
# can better recognize when a perf regression is introduced.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
37
39
|
rescue Exception
|
|
38
40
|
@lock.synchronize {
|
|
39
41
|
@jobs["#{klass}|f"] += 1
|
|
@@ -48,8 +50,8 @@ module Sidekiq
|
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
LONG_TERM = 90 * 24 * 60 * 60
|
|
52
|
-
MID_TERM =
|
|
53
|
+
# LONG_TERM = 90 * 24 * 60 * 60
|
|
54
|
+
MID_TERM = 3 * 24 * 60 * 60
|
|
53
55
|
SHORT_TERM = 8 * 60 * 60
|
|
54
56
|
|
|
55
57
|
def flush(time = Time.now)
|
|
@@ -59,12 +61,15 @@ module Sidekiq
|
|
|
59
61
|
return if procd == 0 && fails == 0
|
|
60
62
|
|
|
61
63
|
now = time.utc
|
|
62
|
-
nowdate = now.strftime("%Y%m%d")
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
# nowdate = now.strftime("%Y%m%d")
|
|
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")
|
|
65
69
|
count = 0
|
|
66
70
|
|
|
67
71
|
redis do |conn|
|
|
72
|
+
# persist fine-grained histogram data
|
|
68
73
|
if grams.size > 0
|
|
69
74
|
conn.pipelined do |pipe|
|
|
70
75
|
grams.each do |_, gram|
|
|
@@ -73,15 +78,16 @@ module Sidekiq
|
|
|
73
78
|
end
|
|
74
79
|
end
|
|
75
80
|
|
|
81
|
+
# persist coarse grained execution count + execution millis.
|
|
82
|
+
# note as of today we don't use or do anything with the
|
|
83
|
+
# daily or hourly rollups.
|
|
76
84
|
[
|
|
77
|
-
["j", jobs, nowdate, LONG_TERM],
|
|
78
|
-
["j", jobs,
|
|
79
|
-
["j", jobs,
|
|
85
|
+
# ["j", jobs, nowdate, LONG_TERM],
|
|
86
|
+
["j", jobs, nowmid, MID_TERM],
|
|
87
|
+
["j", jobs, nowshort, SHORT_TERM]
|
|
80
88
|
].each do |prefix, data, bucket, ttl|
|
|
81
|
-
# Quietly seed the new 7.0 stats format so migration is painless.
|
|
82
89
|
conn.pipelined do |xa|
|
|
83
90
|
stats = "#{prefix}|#{bucket}"
|
|
84
|
-
# logger.debug "Flushing metrics #{stats}"
|
|
85
91
|
data.each_pair do |key, value|
|
|
86
92
|
xa.hincrby stats, key, value
|
|
87
93
|
count += 1
|
|
@@ -89,22 +95,34 @@ module Sidekiq
|
|
|
89
95
|
xa.expire(stats, ttl)
|
|
90
96
|
end
|
|
91
97
|
end
|
|
92
|
-
logger.
|
|
98
|
+
logger.debug "Flushed #{count} metrics"
|
|
93
99
|
count
|
|
94
100
|
end
|
|
95
101
|
end
|
|
96
102
|
|
|
97
103
|
private
|
|
98
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
|
+
|
|
99
113
|
def reset
|
|
100
114
|
@lock.synchronize {
|
|
101
115
|
array = [@totals, @jobs, @grams]
|
|
102
|
-
|
|
103
|
-
@jobs = Hash.new(0)
|
|
104
|
-
@grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
|
|
116
|
+
reset_instance_variables
|
|
105
117
|
array
|
|
106
118
|
}
|
|
107
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
|
|
108
126
|
end
|
|
109
127
|
|
|
110
128
|
class Middleware
|
|
@@ -121,14 +139,15 @@ module Sidekiq
|
|
|
121
139
|
end
|
|
122
140
|
end
|
|
123
141
|
|
|
124
|
-
|
|
125
|
-
Sidekiq.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
Sidekiq.configure_server do |config|
|
|
143
|
+
exec = Sidekiq::Metrics::ExecutionTracker.new(config)
|
|
144
|
+
config.server_middleware do |chain|
|
|
145
|
+
chain.add Sidekiq::Metrics::Middleware, exec
|
|
146
|
+
end
|
|
147
|
+
config.on(:beat) do
|
|
148
|
+
exec.flush
|
|
149
|
+
end
|
|
150
|
+
config.on(:exit) do
|
|
151
|
+
exec.flush
|
|
133
152
|
end
|
|
134
153
|
end
|
|
@@ -80,15 +80,6 @@ module Sidekiq
|
|
|
80
80
|
class Chain
|
|
81
81
|
include Enumerable
|
|
82
82
|
|
|
83
|
-
# A unique instance of the middleware chain is created for
|
|
84
|
-
# each job executed in order to be thread-safe.
|
|
85
|
-
# @param copy [Sidekiq::Middleware::Chain] New instance of Chain
|
|
86
|
-
# @returns nil
|
|
87
|
-
def initialize_copy(copy)
|
|
88
|
-
copy.instance_variable_set(:@entries, entries.dup)
|
|
89
|
-
nil
|
|
90
|
-
end
|
|
91
|
-
|
|
92
83
|
# Iterate through each middleware in the chain
|
|
93
84
|
def each(&block)
|
|
94
85
|
entries.each(&block)
|
|
@@ -105,6 +96,12 @@ module Sidekiq
|
|
|
105
96
|
@entries ||= []
|
|
106
97
|
end
|
|
107
98
|
|
|
99
|
+
def copy_for(capsule)
|
|
100
|
+
chain = Sidekiq::Middleware::Chain.new(capsule)
|
|
101
|
+
chain.instance_variable_set(:@entries, entries.dup)
|
|
102
|
+
chain
|
|
103
|
+
end
|
|
104
|
+
|
|
108
105
|
# Remove all middleware matching the given Class
|
|
109
106
|
# @param klass [Class]
|
|
110
107
|
def remove(klass)
|
|
@@ -152,6 +149,7 @@ module Sidekiq
|
|
|
152
149
|
def exists?(klass)
|
|
153
150
|
any? { |entry| entry.klass == klass }
|
|
154
151
|
end
|
|
152
|
+
alias_method :include?, :exists?
|
|
155
153
|
|
|
156
154
|
# @return [Boolean] if the chain contains no middleware
|
|
157
155
|
def empty?
|
|
@@ -168,23 +166,26 @@ module Sidekiq
|
|
|
168
166
|
|
|
169
167
|
# Used by Sidekiq to execute the middleware at runtime
|
|
170
168
|
# @api private
|
|
171
|
-
def invoke(*args)
|
|
169
|
+
def invoke(*args, &block)
|
|
172
170
|
return yield if empty?
|
|
173
171
|
|
|
174
172
|
chain = retrieve
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
traverse(chain, 0, args, &block)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def traverse(chain, index, args, &block)
|
|
179
|
+
if index >= chain.size
|
|
180
|
+
yield
|
|
181
|
+
else
|
|
182
|
+
chain[index].call(*args) do
|
|
183
|
+
traverse(chain, index + 1, args, &block)
|
|
180
184
|
end
|
|
181
185
|
end
|
|
182
|
-
traverse_chain.call
|
|
183
186
|
end
|
|
184
187
|
end
|
|
185
188
|
|
|
186
|
-
private
|
|
187
|
-
|
|
188
189
|
# Represents each link in the middleware chain
|
|
189
190
|
# @api private
|
|
190
191
|
class Entry
|
|
@@ -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
|
|
@@ -7,27 +10,33 @@ module Sidekiq
|
|
|
7
10
|
# This can be useful for multi-tenancy, i18n locale, timezone, any implicit
|
|
8
11
|
# per-request attribute. See +ActiveSupport::CurrentAttributes+.
|
|
9
12
|
#
|
|
13
|
+
# For multiple current attributes, pass an array of current attributes.
|
|
14
|
+
#
|
|
10
15
|
# @example
|
|
11
16
|
#
|
|
12
17
|
# # in your initializer
|
|
13
18
|
# require "sidekiq/middleware/current_attributes"
|
|
14
19
|
# Sidekiq::CurrentAttributes.persist("Myapp::Current")
|
|
20
|
+
# # or multiple current attributes
|
|
21
|
+
# Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
|
|
15
22
|
#
|
|
16
23
|
module CurrentAttributes
|
|
24
|
+
Serializer = ::ActiveJob::Arguments
|
|
25
|
+
|
|
17
26
|
class Save
|
|
18
27
|
include Sidekiq::ClientMiddleware
|
|
19
28
|
|
|
20
|
-
def initialize(
|
|
21
|
-
@
|
|
29
|
+
def initialize(cattrs)
|
|
30
|
+
@cattrs = cattrs
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
def call(_, job, _, _)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
job
|
|
29
|
-
|
|
30
|
-
job[
|
|
34
|
+
@cattrs.each do |(key, strklass)|
|
|
35
|
+
if !job.has_key?(key)
|
|
36
|
+
attrs = strklass.constantize.attributes
|
|
37
|
+
# Retries can push the job N times, we don't
|
|
38
|
+
# want retries to reset cattr. #5692, #5090
|
|
39
|
+
job[key] = Serializer.serialize(attrs) if attrs.any?
|
|
31
40
|
end
|
|
32
41
|
end
|
|
33
42
|
yield
|
|
@@ -37,26 +46,74 @@ module Sidekiq
|
|
|
37
46
|
class Load
|
|
38
47
|
include Sidekiq::ServerMiddleware
|
|
39
48
|
|
|
40
|
-
def initialize(
|
|
41
|
-
@
|
|
49
|
+
def initialize(cattrs)
|
|
50
|
+
@cattrs = cattrs
|
|
42
51
|
end
|
|
43
52
|
|
|
44
|
-
def call(_, job,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
def call(_, job, *, &block)
|
|
54
|
+
klass_attrs = {}
|
|
55
|
+
|
|
56
|
+
@cattrs.each do |(key, strklass)|
|
|
57
|
+
next unless job.has_key?(key)
|
|
58
|
+
|
|
59
|
+
klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
|
|
60
|
+
end
|
|
61
|
+
|
|
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
|
|
49
88
|
end
|
|
50
89
|
end
|
|
51
90
|
end
|
|
52
91
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
92
|
+
class << self
|
|
93
|
+
def persist(klass_or_array, config = Sidekiq.default_configuration)
|
|
94
|
+
cattrs = build_cattrs_hash(klass_or_array)
|
|
95
|
+
|
|
96
|
+
config.client_middleware.prepend Load, cattrs
|
|
97
|
+
config.client_middleware.add Save, cattrs
|
|
98
|
+
config.server_middleware.prepend Load, cattrs
|
|
56
99
|
end
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def build_cattrs_hash(klass_or_array)
|
|
104
|
+
if klass_or_array.is_a?(Array)
|
|
105
|
+
{}.tap do |hash|
|
|
106
|
+
klass_or_array.each_with_index do |klass, index|
|
|
107
|
+
hash[key_at(index)] = klass.to_s
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
{key_at(0) => klass_or_array.to_s}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def key_at(index)
|
|
116
|
+
(index == 0) ? "cattr" : "cattr_#{index}"
|
|
60
117
|
end
|
|
61
118
|
end
|
|
62
119
|
end
|