sidekiq 6.5.2 → 6.5.5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sidekiq might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Changes.md +17 -0
- data/lib/sidekiq/api.rb +5 -2
- data/lib/sidekiq/cli.rb +1 -1
- data/lib/sidekiq/job_retry.rb +13 -8
- data/lib/sidekiq/metrics/deploy.rb +2 -2
- data/lib/sidekiq/metrics/query.rb +102 -73
- data/lib/sidekiq/processor.rb +8 -2
- data/lib/sidekiq/scheduled.rb +1 -7
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +3 -3
- data/lib/sidekiq/web/application.rb +7 -7
- data/lib/sidekiq/web.rb +1 -1
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/metrics.js +262 -0
- data/web/assets/stylesheets/application.css +44 -1
- data/web/locales/el.yml +43 -19
- data/web/locales/en.yml +4 -1
- data/web/views/metrics.erb +47 -37
- data/web/views/metrics_for_job.erb +77 -82
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56d317d1dc6e27057146cee5e5951030fb907489025a0d14f1e11c7ae952befe
|
4
|
+
data.tar.gz: 7626890a494eb9f55374a24fa2dc0cf69193ddf31c00ef9a96c15e3a6aa88450
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 11fa0967bb8dcd19aa7c5893d151500ac2b8f06fb364fd957d5a01a8fe6842247e6da3b45d7103e95026ae7741265ef84881278bc55d6c1830480ee9e83cadd6
|
7
|
+
data.tar.gz: d625912d1f148c56b82da498cac73109532050d4e36c51ac9c3b229207a0e32ecbd34d3bc53823703951fa0ead9304113bdee654b8898d4cc2e4973621b8a268
|
data/Changes.md
CHANGED
@@ -2,6 +2,23 @@
|
|
2
2
|
|
3
3
|
[Sidekiq Changes](https://github.com/mperham/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/mperham/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/mperham/sidekiq/blob/main/Ent-Changes.md)
|
4
4
|
|
5
|
+
6.5.5
|
6
|
+
----------
|
7
|
+
|
8
|
+
- Fix require issue with job_retry.rb [#5462]
|
9
|
+
- Improve Sidekiq::Web compatibility with Rack 3.x
|
10
|
+
|
11
|
+
6.5.4
|
12
|
+
----------
|
13
|
+
|
14
|
+
- Fix invalid code on Ruby 2.5 [#5460]
|
15
|
+
- Fix further metrics dependency issues [#5457]
|
16
|
+
|
17
|
+
6.5.3
|
18
|
+
----------
|
19
|
+
|
20
|
+
- Don't require metrics code without explicit opt-in [#5456]
|
21
|
+
|
5
22
|
6.5.2
|
6
23
|
----------
|
7
24
|
|
data/lib/sidekiq/api.rb
CHANGED
@@ -5,8 +5,11 @@ require "sidekiq"
|
|
5
5
|
require "zlib"
|
6
6
|
require "set"
|
7
7
|
require "base64"
|
8
|
-
|
9
|
-
|
8
|
+
|
9
|
+
if ENV["SIDEKIQ_METRICS_BETA"]
|
10
|
+
require "sidekiq/metrics/deploy"
|
11
|
+
require "sidekiq/metrics/query"
|
12
|
+
end
|
10
13
|
|
11
14
|
module Sidekiq
|
12
15
|
# Retrieve runtime statistics from Redis regarding
|
data/lib/sidekiq/cli.rb
CHANGED
data/lib/sidekiq/job_retry.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "sidekiq/scheduled"
|
4
|
-
require "sidekiq/api"
|
5
|
-
|
6
3
|
require "zlib"
|
7
4
|
require "base64"
|
5
|
+
require "sidekiq/component"
|
8
6
|
|
9
7
|
module Sidekiq
|
10
8
|
##
|
@@ -203,14 +201,13 @@ module Sidekiq
|
|
203
201
|
nil
|
204
202
|
end
|
205
203
|
|
206
|
-
delay =
|
207
|
-
|
204
|
+
delay = (count**4) + 15
|
205
|
+
if Integer === rv && rv > 0
|
206
|
+
delay = rv
|
208
207
|
elsif rv == :discard
|
209
208
|
return [:discard, nil] # do nothing, job goes poof
|
210
209
|
elsif rv == :kill
|
211
210
|
return [:kill, nil]
|
212
|
-
else
|
213
|
-
(count**4) + 15
|
214
211
|
end
|
215
212
|
|
216
213
|
[:default, delay]
|
@@ -236,7 +233,15 @@ module Sidekiq
|
|
236
233
|
def send_to_morgue(msg)
|
237
234
|
logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
|
238
235
|
payload = Sidekiq.dump_json(msg)
|
239
|
-
|
236
|
+
now = Time.now.to_f
|
237
|
+
|
238
|
+
config.redis do |conn|
|
239
|
+
conn.multi do |xa|
|
240
|
+
xa.zadd("dead", now.to_s, payload)
|
241
|
+
xa.zremrangebyscore("dead", "-inf", now - config[:dead_timeout_in_seconds])
|
242
|
+
xa.zremrangebyrank("dead", 0, - config[:dead_max_jobs])
|
243
|
+
end
|
244
|
+
end
|
240
245
|
end
|
241
246
|
|
242
247
|
def retry_attempts_from(msg_retry, default)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require "sidekiq"
|
2
|
-
require "
|
2
|
+
require "time"
|
3
3
|
|
4
4
|
# This file is designed to be required within the user's
|
5
5
|
# deployment script; it should need a bare minimum of dependencies.
|
@@ -32,7 +32,7 @@ module Sidekiq
|
|
32
32
|
key = "#{datecode}-marks"
|
33
33
|
@pool.with do |c|
|
34
34
|
c.pipelined do |pipe|
|
35
|
-
pipe.hsetnx(key, floor.
|
35
|
+
pipe.hsetnx(key, floor.iso8601, label)
|
36
36
|
pipe.expire(key, MARK_TTL)
|
37
37
|
end
|
38
38
|
end
|
@@ -13,111 +13,140 @@ module Sidekiq
|
|
13
13
|
# NB: all metrics and times/dates are UTC only. We specifically do not
|
14
14
|
# support timezones.
|
15
15
|
class Query
|
16
|
-
# :hour, :day, :month
|
17
|
-
attr_accessor :period
|
18
|
-
|
19
|
-
# a specific job class, e.g. "App::OrderJob"
|
20
|
-
attr_accessor :klass
|
21
|
-
|
22
|
-
# the date specific to the period
|
23
|
-
# for :day or :hour, something like Date.today or Date.new(2022, 7, 13)
|
24
|
-
# for :month, Date.new(2022, 7, 1)
|
25
|
-
attr_accessor :date
|
26
|
-
|
27
|
-
# for period = :hour, the specific hour, integer e.g. 1 or 18
|
28
|
-
# note that hours and minutes do not have a leading zero so minute-specific
|
29
|
-
# keys will look like "j|20220718|7:3" for data at 07:03.
|
30
|
-
attr_accessor :hour
|
31
|
-
|
32
16
|
def initialize(pool: Sidekiq.redis_pool, now: Time.now)
|
33
17
|
@time = now.utc
|
34
18
|
@pool = pool
|
35
19
|
@klass = nil
|
36
20
|
end
|
37
21
|
|
38
|
-
# Get metric data from the last hour
|
39
|
-
|
40
|
-
|
41
|
-
resultset = {}
|
42
|
-
resultset[:date] = @time.to_date
|
43
|
-
resultset[:period] = :hour
|
44
|
-
resultset[:ends_at] = @time
|
45
|
-
time = @time
|
22
|
+
# Get metric data for all jobs from the last hour
|
23
|
+
def top_jobs(minutes: 60)
|
24
|
+
result = Result.new
|
46
25
|
|
47
|
-
|
26
|
+
time = @time
|
27
|
+
redis_results = @pool.with do |conn|
|
48
28
|
conn.pipelined do |pipe|
|
49
|
-
|
50
|
-
60.times do |idx|
|
29
|
+
minutes.times do |idx|
|
51
30
|
key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
|
52
31
|
pipe.hgetall key
|
32
|
+
result.prepend_bucket time
|
53
33
|
time -= 60
|
54
34
|
end
|
55
|
-
resultset[:starts_at] = time
|
56
35
|
end
|
57
36
|
end
|
58
37
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
resultset[:job_classes] = klsset.delete_if { |item| item.size < 3 }
|
67
|
-
resultset[:totals] = t
|
68
|
-
top = t.each_with_object({}) do |(k, v), memo|
|
69
|
-
(kls, metric) = k.split("|")
|
70
|
-
memo[metric] ||= Hash.new(0)
|
71
|
-
memo[metric][kls] = v
|
38
|
+
time = @time
|
39
|
+
redis_results.each do |hash|
|
40
|
+
hash.each do |k, v|
|
41
|
+
kls, metric = k.split("|")
|
42
|
+
result.job_results[kls].add_metric metric, time, v.to_i
|
43
|
+
end
|
44
|
+
time -= 60
|
72
45
|
end
|
73
46
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
end
|
78
|
-
resultset[:top_classes] = sorted
|
79
|
-
resultset
|
47
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
48
|
+
|
49
|
+
result
|
80
50
|
end
|
81
51
|
|
82
|
-
def for_job(klass)
|
83
|
-
|
84
|
-
resultset[:date] = @time.to_date
|
85
|
-
resultset[:period] = :hour
|
86
|
-
resultset[:ends_at] = @time
|
87
|
-
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
52
|
+
def for_job(klass, minutes: 60)
|
53
|
+
result = Result.new
|
88
54
|
|
89
55
|
time = @time
|
90
|
-
|
56
|
+
redis_results = @pool.with do |conn|
|
91
57
|
conn.pipelined do |pipe|
|
92
|
-
|
93
|
-
|
94
|
-
key = "j|#{time.strftime("%Y%m%d|%-H:%-M")}"
|
58
|
+
minutes.times do |idx|
|
59
|
+
key = "j|#{time.strftime("%Y%m%d")}|#{time.hour}:#{time.min}"
|
95
60
|
pipe.hmget key, "#{klass}|ms", "#{klass}|p", "#{klass}|f"
|
61
|
+
result.prepend_bucket time
|
96
62
|
time -= 60
|
97
63
|
end
|
98
64
|
end
|
99
65
|
end
|
100
66
|
|
101
67
|
time = @time
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
68
|
+
@pool.with do |conn|
|
69
|
+
redis_results.each do |(ms, p, f)|
|
70
|
+
result.job_results[klass].add_metric "ms", time, ms.to_i if ms
|
71
|
+
result.job_results[klass].add_metric "p", time, p.to_i if p
|
72
|
+
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 -= 60
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
result.marks = fetch_marks(result.starts_at..result.ends_at)
|
79
|
+
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
class Result < Struct.new(:starts_at, :ends_at, :size, :buckets, :job_results, :marks)
|
84
|
+
def initialize
|
85
|
+
super
|
86
|
+
self.buckets = []
|
87
|
+
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
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class JobResult < Struct.new(:series, :hist, :totals)
|
99
|
+
def initialize
|
100
|
+
super
|
101
|
+
self.series = Hash.new { |h, k| h[k] = Hash.new(0) }
|
102
|
+
self.hist = Hash.new { |h, k| h[k] = [] }
|
103
|
+
self.totals = Hash.new(0)
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_metric(metric, time, value)
|
107
|
+
totals[metric] += value
|
108
|
+
series[metric][time.strftime("%H:%M")] += value
|
109
|
+
|
110
|
+
# Include timing measurements in seconds for convenience
|
111
|
+
add_metric("s", time, value / 1000.0) if metric == "ms"
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_hist(time, hist_result)
|
115
|
+
hist[time.strftime("%H:%M")] = hist_result
|
116
|
+
end
|
117
|
+
|
118
|
+
def total_avg(metric = "ms")
|
119
|
+
completed = totals["p"] - totals["f"]
|
120
|
+
totals[metric].to_f / completed
|
121
|
+
end
|
122
|
+
|
123
|
+
def series_avg(metric = "ms")
|
124
|
+
series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
|
125
|
+
completed = series.dig("p", bucket) - series.dig("f", bucket)
|
126
|
+
result[bucket] = completed == 0 ? 0 : value.to_f / completed
|
114
127
|
end
|
115
128
|
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class MarkResult < Struct.new(:time, :label)
|
132
|
+
def bucket
|
133
|
+
time.strftime("%H:%M")
|
134
|
+
end
|
135
|
+
end
|
116
136
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
137
|
+
private
|
138
|
+
|
139
|
+
def fetch_marks(time_range)
|
140
|
+
[].tap do |result|
|
141
|
+
marks = @pool.with { |c| c.hgetall("#{@time.strftime("%Y%m%d")}-marks") }
|
142
|
+
|
143
|
+
marks.each do |timestamp, label|
|
144
|
+
time = Time.parse(timestamp)
|
145
|
+
if time_range.cover? time
|
146
|
+
result << MarkResult.new(time, label)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
121
150
|
end
|
122
151
|
end
|
123
152
|
end
|
data/lib/sidekiq/processor.rb
CHANGED
@@ -152,8 +152,14 @@ module Sidekiq
|
|
152
152
|
job_hash = Sidekiq.load_json(jobstr)
|
153
153
|
rescue => ex
|
154
154
|
handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
|
155
|
-
|
156
|
-
|
155
|
+
now = Time.now.to_f
|
156
|
+
config.redis do |conn|
|
157
|
+
conn.multi do |xa|
|
158
|
+
xa.zadd("dead", now.to_s, jobstr)
|
159
|
+
xa.zremrangebyscore("dead", "-inf", now - config[:dead_timeout_in_seconds])
|
160
|
+
xa.zremrangebyrank("dead", 0, - config[:dead_max_jobs])
|
161
|
+
end
|
162
|
+
end
|
157
163
|
return uow.acknowledge
|
158
164
|
end
|
159
165
|
|
data/lib/sidekiq/scheduled.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "sidekiq"
|
4
|
-
require "sidekiq/api"
|
5
4
|
require "sidekiq/component"
|
6
5
|
|
7
6
|
module Sidekiq
|
@@ -183,13 +182,8 @@ module Sidekiq
|
|
183
182
|
end
|
184
183
|
|
185
184
|
def process_count
|
186
|
-
|
187
|
-
# expensive at scale. Cut it down by 90% with this counter.
|
188
|
-
# NB: This method is only called by the scheduler thread so we
|
189
|
-
# don't need to worry about the thread safety of +=.
|
190
|
-
pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
|
185
|
+
pcount = Sidekiq.redis { |conn| conn.scard("processes") }
|
191
186
|
pcount = 1 if pcount == 0
|
192
|
-
@count_calls += 1
|
193
187
|
pcount
|
194
188
|
end
|
195
189
|
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
@@ -15,11 +15,11 @@ module Sidekiq
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def halt(res)
|
18
|
-
throw :halt, [res, {"
|
18
|
+
throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
|
19
19
|
end
|
20
20
|
|
21
21
|
def redirect(location)
|
22
|
-
throw :halt, [302, {"
|
22
|
+
throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
|
23
23
|
end
|
24
24
|
|
25
25
|
def params
|
@@ -68,7 +68,7 @@ module Sidekiq
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def json(payload)
|
71
|
-
[200, {"
|
71
|
+
[200, {"content-type" => "application/json", "cache-control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
|
72
72
|
end
|
73
73
|
|
74
74
|
def initialize(env, block)
|
@@ -62,14 +62,14 @@ module Sidekiq
|
|
62
62
|
|
63
63
|
get "/metrics" do
|
64
64
|
q = Sidekiq::Metrics::Query.new
|
65
|
-
@
|
65
|
+
@query_result = q.top_jobs
|
66
66
|
erb(:metrics)
|
67
67
|
end
|
68
68
|
|
69
69
|
get "/metrics/:name" do
|
70
70
|
@name = route_params[:name]
|
71
71
|
q = Sidekiq::Metrics::Query.new
|
72
|
-
@
|
72
|
+
@query_result = q.for_job(@name)
|
73
73
|
erb(:metrics_for_job)
|
74
74
|
end
|
75
75
|
|
@@ -312,7 +312,7 @@ module Sidekiq
|
|
312
312
|
|
313
313
|
def call(env)
|
314
314
|
action = self.class.match(env)
|
315
|
-
return [404, {"
|
315
|
+
return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
|
316
316
|
|
317
317
|
app = @klass
|
318
318
|
resp = catch(:halt) do
|
@@ -329,10 +329,10 @@ module Sidekiq
|
|
329
329
|
else
|
330
330
|
# rendered content goes here
|
331
331
|
headers = {
|
332
|
-
"
|
333
|
-
"
|
334
|
-
"
|
335
|
-
"
|
332
|
+
"content-type" => "text/html",
|
333
|
+
"cache-control" => "private, no-store",
|
334
|
+
"content-language" => action.locale,
|
335
|
+
"content-security-policy" => CSP_HEADER
|
336
336
|
}
|
337
337
|
# we'll let Rack calculate Content-Length for us.
|
338
338
|
[200, headers, [resp]]
|
data/lib/sidekiq/web.rb
CHANGED
@@ -148,7 +148,7 @@ module Sidekiq
|
|
148
148
|
m = middlewares
|
149
149
|
|
150
150
|
rules = []
|
151
|
-
rules = [[:all, {"
|
151
|
+
rules = [[:all, {"cache-control" => "public, max-age=86400"}]] unless ENV["SIDEKIQ_WEB_TESTING"]
|
152
152
|
|
153
153
|
::Rack::Builder.new do
|
154
154
|
use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
|