sidekiq 7.0.0 → 7.3.0
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 +261 -13
- data/README.md +34 -27
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiqload +204 -109
- data/bin/sidekiqmon +3 -0
- data/lib/sidekiq/api.rb +151 -23
- data/lib/sidekiq/capsule.rb +20 -0
- data/lib/sidekiq/cli.rb +9 -4
- data/lib/sidekiq/client.rb +40 -24
- data/lib/sidekiq/component.rb +3 -1
- data/lib/sidekiq/config.rb +32 -12
- data/lib/sidekiq/deploy.rb +5 -5
- data/lib/sidekiq/embedded.rb +3 -3
- data/lib/sidekiq/fetch.rb +3 -5
- data/lib/sidekiq/iterable_job.rb +53 -0
- data/lib/sidekiq/job/interrupt_handler.rb +22 -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 +231 -0
- data/lib/sidekiq/job.rb +17 -10
- data/lib/sidekiq/job_logger.rb +24 -11
- data/lib/sidekiq/job_retry.rb +34 -11
- data/lib/sidekiq/job_util.rb +51 -15
- data/lib/sidekiq/launcher.rb +38 -22
- data/lib/sidekiq/logger.rb +1 -1
- data/lib/sidekiq/metrics/query.rb +6 -3
- data/lib/sidekiq/metrics/shared.rb +4 -4
- data/lib/sidekiq/metrics/tracking.rb +9 -3
- data/lib/sidekiq/middleware/chain.rb +12 -9
- data/lib/sidekiq/middleware/current_attributes.rb +70 -17
- data/lib/sidekiq/monitor.rb +17 -4
- data/lib/sidekiq/paginator.rb +4 -4
- data/lib/sidekiq/processor.rb +41 -27
- data/lib/sidekiq/rails.rb +18 -8
- data/lib/sidekiq/redis_client_adapter.rb +31 -35
- data/lib/sidekiq/redis_connection.rb +29 -7
- data/lib/sidekiq/scheduled.rb +4 -4
- data/lib/sidekiq/testing.rb +27 -8
- data/lib/sidekiq/transaction_aware_client.rb +7 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +10 -4
- data/lib/sidekiq/web/application.rb +113 -16
- data/lib/sidekiq/web/csrf_protection.rb +9 -6
- data/lib/sidekiq/web/helpers.rb +104 -33
- data/lib/sidekiq/web.rb +63 -2
- data/lib/sidekiq.rb +2 -1
- data/sidekiq.gemspec +8 -29
- data/web/assets/javascripts/application.js +45 -0
- data/web/assets/javascripts/dashboard-charts.js +38 -12
- data/web/assets/javascripts/dashboard.js +8 -10
- data/web/assets/javascripts/metrics.js +64 -2
- data/web/assets/stylesheets/application-dark.css +4 -0
- data/web/assets/stylesheets/application-rtl.css +10 -0
- data/web/assets/stylesheets/application.css +38 -4
- data/web/locales/da.yml +11 -4
- data/web/locales/en.yml +2 -0
- data/web/locales/fr.yml +14 -0
- data/web/locales/gd.yml +99 -0
- data/web/locales/ja.yml +3 -1
- data/web/locales/pt-br.yml +20 -0
- data/web/locales/tr.yml +101 -0
- data/web/locales/zh-cn.yml +20 -19
- data/web/views/_footer.erb +14 -2
- data/web/views/_job_info.erb +18 -2
- data/web/views/_metrics_period_select.erb +12 -0
- data/web/views/_paging.erb +2 -0
- data/web/views/_poll_link.erb +1 -1
- data/web/views/_summary.erb +7 -7
- data/web/views/busy.erb +46 -35
- data/web/views/dashboard.erb +25 -35
- data/web/views/filtering.erb +7 -0
- data/web/views/layout.erb +6 -6
- data/web/views/metrics.erb +42 -31
- data/web/views/metrics_for_job.erb +41 -51
- data/web/views/morgue.erb +5 -9
- data/web/views/queue.erb +10 -14
- data/web/views/queues.erb +9 -3
- data/web/views/retries.erb +5 -9
- data/web/views/scheduled.erb +12 -13
- metadata +37 -32
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
3
4
|
require "redis_client"
|
4
5
|
require "redis_client/decorator"
|
5
6
|
|
@@ -8,6 +9,9 @@ module Sidekiq
|
|
8
9
|
BaseError = RedisClient::Error
|
9
10
|
CommandError = RedisClient::CommandError
|
10
11
|
|
12
|
+
# You can add/remove items or clear the whole thing if you don't want deprecation warnings.
|
13
|
+
DEPRECATED_COMMANDS = %i[rpoplpush zrangebyscore zrevrange zrevrangebyscore getset hmset setex setnx].to_set
|
14
|
+
|
11
15
|
module CompatMethods
|
12
16
|
def info
|
13
17
|
@client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
|
@@ -17,11 +21,28 @@ module Sidekiq
|
|
17
21
|
@client.call("EVALSHA", sha, keys.size, *keys, *argv)
|
18
22
|
end
|
19
23
|
|
24
|
+
# this is the set of Redis commands used by Sidekiq. Not guaranteed
|
25
|
+
# to be comprehensive, we use this as a performance enhancement to
|
26
|
+
# avoid calling method_missing on most commands
|
27
|
+
USED_COMMANDS = %w[bitfield bitfield_ro del exists expire flushdb
|
28
|
+
get hdel hget hgetall hincrby hlen hmget hset hsetnx incr incrby
|
29
|
+
lindex llen lmove lpop lpush lrange lrem mget mset ping pttl
|
30
|
+
publish rpop rpush sadd scard script set sismember smembers
|
31
|
+
srem ttl type unlink zadd zcard zincrby zrange zrem
|
32
|
+
zremrangebyrank zremrangebyscore]
|
33
|
+
|
34
|
+
USED_COMMANDS.each do |name|
|
35
|
+
define_method(name) do |*args, **kwargs|
|
36
|
+
@client.call(name, *args, **kwargs)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
20
40
|
private
|
21
41
|
|
22
42
|
# this allows us to use methods like `conn.hmset(...)` instead of having to use
|
23
43
|
# redis-client's native `conn.call("hmset", ...)`
|
24
44
|
def method_missing(*args, &block)
|
45
|
+
warn("[sidekiq#5788] Redis has deprecated the `#{args.first}`command, called at #{caller(1..1)}") if DEPRECATED_COMMANDS.include?(args.first)
|
25
46
|
@client.call(*args, *block)
|
26
47
|
end
|
27
48
|
ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
|
@@ -34,42 +55,22 @@ module Sidekiq
|
|
34
55
|
CompatClient = RedisClient::Decorator.create(CompatMethods)
|
35
56
|
|
36
57
|
class CompatClient
|
37
|
-
|
38
|
-
def _client
|
39
|
-
@client
|
40
|
-
end
|
41
|
-
|
42
|
-
def _config
|
58
|
+
def config
|
43
59
|
@client.config
|
44
60
|
end
|
45
|
-
|
46
|
-
def message
|
47
|
-
yield nil, @queue.pop
|
48
|
-
end
|
49
|
-
|
50
|
-
# NB: this method does not return
|
51
|
-
def subscribe(chan)
|
52
|
-
@queue = ::Queue.new
|
53
|
-
|
54
|
-
pubsub = @client.pubsub
|
55
|
-
pubsub.call("subscribe", chan)
|
56
|
-
|
57
|
-
loop do
|
58
|
-
evt = pubsub.next_event
|
59
|
-
next if evt.nil?
|
60
|
-
next unless evt[0] == "message" && evt[1] == chan
|
61
|
-
|
62
|
-
(_, _, msg) = evt
|
63
|
-
@queue << msg
|
64
|
-
yield self
|
65
|
-
end
|
66
|
-
end
|
67
61
|
end
|
68
62
|
|
69
63
|
def initialize(options)
|
70
64
|
opts = client_opts(options)
|
71
65
|
@config = if opts.key?(:sentinels)
|
72
66
|
RedisClient.sentinel(**opts)
|
67
|
+
elsif opts.key?(:nodes)
|
68
|
+
# Sidekiq does not support Redis clustering but Sidekiq Enterprise's
|
69
|
+
# rate limiters are cluster-safe so we can scale to millions
|
70
|
+
# of rate limiters using a Redis cluster. This requires the
|
71
|
+
# `redis-cluster-client` gem.
|
72
|
+
# Sidekiq::Limiter.redis = { nodes: [...] }
|
73
|
+
RedisClient.cluster(**opts)
|
73
74
|
else
|
74
75
|
RedisClient.config(**opts)
|
75
76
|
end
|
@@ -85,8 +86,7 @@ module Sidekiq
|
|
85
86
|
opts = options.dup
|
86
87
|
|
87
88
|
if opts[:namespace]
|
88
|
-
raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature
|
89
|
-
"Either use the redis adapter or remove the namespace."
|
89
|
+
raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature is no longer supported in Sidekiq 7+. See https://github.com/sidekiq/sidekiq/blob/main/docs/7.0-Upgrade.md#redis-namespace."
|
90
90
|
end
|
91
91
|
|
92
92
|
opts.delete(:size)
|
@@ -97,13 +97,9 @@ module Sidekiq
|
|
97
97
|
opts.delete(:network_timeout)
|
98
98
|
end
|
99
99
|
|
100
|
-
if opts[:driver]
|
101
|
-
opts[:driver] = opts[:driver].to_sym
|
102
|
-
end
|
103
|
-
|
104
100
|
opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
|
105
101
|
opts[:role] = opts[:role].to_sym if opts.key?(:role)
|
106
|
-
opts
|
102
|
+
opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
|
107
103
|
|
108
104
|
# Issue #3303, redis-rb will silently retry an operation.
|
109
105
|
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
@@ -8,16 +8,28 @@ module Sidekiq
|
|
8
8
|
module RedisConnection
|
9
9
|
class << self
|
10
10
|
def create(options = {})
|
11
|
-
symbolized_options = options
|
11
|
+
symbolized_options = deep_symbolize_keys(options)
|
12
12
|
symbolized_options[:url] ||= determine_redis_provider
|
13
13
|
|
14
14
|
logger = symbolized_options.delete(:logger)
|
15
15
|
logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
|
16
16
|
|
17
|
+
raise "Sidekiq 7+ does not support Redis protocol 2" if symbolized_options[:protocol] == 2
|
18
|
+
|
19
|
+
safe = !!symbolized_options.delete(:cluster_safe)
|
20
|
+
raise ":nodes not allowed, Sidekiq is not safe to run on Redis Cluster" if !safe && symbolized_options.key?(:nodes)
|
21
|
+
|
17
22
|
size = symbolized_options.delete(:size) || 5
|
18
23
|
pool_timeout = symbolized_options.delete(:pool_timeout) || 1
|
19
24
|
pool_name = symbolized_options.delete(:pool_name)
|
20
25
|
|
26
|
+
# Default timeout in redis-client is 1 second, which can be too aggressive
|
27
|
+
# if the Sidekiq process is CPU-bound. With 10-15 threads and a thread quantum of 100ms,
|
28
|
+
# it can be easy to get the occasional ReadTimeoutError. You can still provide
|
29
|
+
# a smaller timeout explicitly:
|
30
|
+
# config.redis = { url: "...", timeout: 1 }
|
31
|
+
symbolized_options[:timeout] ||= 3
|
32
|
+
|
21
33
|
redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
|
22
34
|
ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
|
23
35
|
redis_config.new_client
|
@@ -26,6 +38,19 @@ module Sidekiq
|
|
26
38
|
|
27
39
|
private
|
28
40
|
|
41
|
+
def deep_symbolize_keys(object)
|
42
|
+
case object
|
43
|
+
when Hash
|
44
|
+
object.each_with_object({}) do |(key, value), result|
|
45
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
46
|
+
end
|
47
|
+
when Array
|
48
|
+
object.map { |e| deep_symbolize_keys(e) }
|
49
|
+
else
|
50
|
+
object
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
29
54
|
def scrub(options)
|
30
55
|
redacted = "REDACTED"
|
31
56
|
|
@@ -38,9 +63,8 @@ module Sidekiq
|
|
38
63
|
uri.password = redacted
|
39
64
|
scrubbed_options[:url] = uri.to_s
|
40
65
|
end
|
41
|
-
if scrubbed_options[:password]
|
42
|
-
|
43
|
-
end
|
66
|
+
scrubbed_options[:password] = redacted if scrubbed_options[:password]
|
67
|
+
scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
|
44
68
|
scrubbed_options[:sentinels]&.each do |sentinel|
|
45
69
|
sentinel[:password] = redacted if sentinel[:password]
|
46
70
|
end
|
@@ -66,9 +90,7 @@ module Sidekiq
|
|
66
90
|
EOM
|
67
91
|
end
|
68
92
|
|
69
|
-
ENV[
|
70
|
-
p || "REDIS_URL"
|
71
|
-
]
|
93
|
+
ENV[p.to_s] || ENV["REDIS_URL"]
|
72
94
|
end
|
73
95
|
end
|
74
96
|
end
|
data/lib/sidekiq/scheduled.rb
CHANGED
@@ -12,7 +12,7 @@ module Sidekiq
|
|
12
12
|
|
13
13
|
LUA_ZPOPBYSCORE = <<~LUA
|
14
14
|
local key, now = KEYS[1], ARGV[1]
|
15
|
-
local jobs = redis.call("
|
15
|
+
local jobs = redis.call("zrange", key, "-inf", now, "byscore", "limit", 0, 1)
|
16
16
|
if jobs[1] then
|
17
17
|
redis.call("zrem", key, jobs[1])
|
18
18
|
return jobs[1]
|
@@ -54,7 +54,7 @@ module Sidekiq
|
|
54
54
|
@lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE)
|
55
55
|
end
|
56
56
|
|
57
|
-
conn.
|
57
|
+
conn.call("EVALSHA", @lua_zpopbyscore_sha, keys.size, *keys, *argv)
|
58
58
|
rescue RedisClient::CommandError => e
|
59
59
|
raise unless e.message.start_with?("NOSCRIPT")
|
60
60
|
|
@@ -144,7 +144,7 @@ module Sidekiq
|
|
144
144
|
# In the example above, each process should schedule every 10 seconds on average. We special
|
145
145
|
# case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
|
146
146
|
# As we run more processes, the scheduling interval average will approach an even spread
|
147
|
-
# between 0 and poll interval so we don't need this
|
147
|
+
# between 0 and poll interval so we don't need this artificial boost.
|
148
148
|
#
|
149
149
|
count = process_count
|
150
150
|
interval = poll_interval_average(count)
|
@@ -193,7 +193,7 @@ module Sidekiq
|
|
193
193
|
# should never depend on sidekiq/api.
|
194
194
|
def cleanup
|
195
195
|
# dont run cleanup more than once per minute
|
196
|
-
return 0 unless redis { |conn| conn.set("process_cleanup", "1",
|
196
|
+
return 0 unless redis { |conn| conn.set("process_cleanup", "1", "NX", "EX", "60") }
|
197
197
|
|
198
198
|
count = 0
|
199
199
|
redis do |conn|
|
data/lib/sidekiq/testing.rb
CHANGED
@@ -5,23 +5,42 @@ require "sidekiq"
|
|
5
5
|
|
6
6
|
module Sidekiq
|
7
7
|
class Testing
|
8
|
+
class TestModeAlreadySetError < RuntimeError; end
|
8
9
|
class << self
|
9
|
-
attr_accessor :
|
10
|
+
attr_accessor :__global_test_mode
|
10
11
|
|
12
|
+
# Calling without a block sets the global test mode, affecting
|
13
|
+
# all threads. Calling with a block only affects the current Thread.
|
11
14
|
def __set_test_mode(mode)
|
12
15
|
if block_given?
|
13
|
-
|
16
|
+
# Reentrant testing modes will lead to a rat's nest of code which is
|
17
|
+
# hard to reason about. You can set the testing mode once globally and
|
18
|
+
# you can override that global setting once per-thread.
|
19
|
+
raise TestModeAlreadySetError, "Nesting test modes is not supported" if __local_test_mode
|
20
|
+
|
21
|
+
self.__local_test_mode = mode
|
14
22
|
begin
|
15
|
-
self.__test_mode = mode
|
16
23
|
yield
|
17
24
|
ensure
|
18
|
-
self.
|
25
|
+
self.__local_test_mode = nil
|
19
26
|
end
|
20
27
|
else
|
21
|
-
self.
|
28
|
+
self.__global_test_mode = mode
|
22
29
|
end
|
23
30
|
end
|
24
31
|
|
32
|
+
def __test_mode
|
33
|
+
__local_test_mode || __global_test_mode
|
34
|
+
end
|
35
|
+
|
36
|
+
def __local_test_mode
|
37
|
+
Thread.current[:__sidekiq_test_mode]
|
38
|
+
end
|
39
|
+
|
40
|
+
def __local_test_mode=(value)
|
41
|
+
Thread.current[:__sidekiq_test_mode] = value
|
42
|
+
end
|
43
|
+
|
25
44
|
def disable!(&block)
|
26
45
|
__set_test_mode(:disable, &block)
|
27
46
|
end
|
@@ -64,7 +83,7 @@ module Sidekiq
|
|
64
83
|
class EmptyQueueError < RuntimeError; end
|
65
84
|
|
66
85
|
module TestingClient
|
67
|
-
def
|
86
|
+
def atomic_push(conn, payloads)
|
68
87
|
if Sidekiq::Testing.fake?
|
69
88
|
payloads.each do |job|
|
70
89
|
job = Sidekiq.load_json(Sidekiq.dump_json(job))
|
@@ -93,7 +112,7 @@ module Sidekiq
|
|
93
112
|
# The Queues class is only for testing the fake queue implementation.
|
94
113
|
# There are 2 data structures involved in tandem. This is due to the
|
95
114
|
# Rspec syntax of change(HardJob.jobs, :size). It keeps a reference
|
96
|
-
# to the array. Because the array was
|
115
|
+
# to the array. Because the array was derived from a filter of the total
|
97
116
|
# jobs enqueued, it appeared as though the array didn't change.
|
98
117
|
#
|
99
118
|
# To solve this, we'll keep 2 hashes containing the jobs. One with keys based
|
@@ -259,7 +278,7 @@ module Sidekiq
|
|
259
278
|
def perform_one
|
260
279
|
raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
|
261
280
|
next_job = jobs.first
|
262
|
-
Queues.delete_for(next_job["jid"], queue, to_s)
|
281
|
+
Queues.delete_for(next_job["jid"], next_job["queue"], to_s)
|
263
282
|
process_job(next_job)
|
264
283
|
end
|
265
284
|
|
@@ -9,7 +9,14 @@ module Sidekiq
|
|
9
9
|
@redis_client = Client.new(pool: pool, config: config)
|
10
10
|
end
|
11
11
|
|
12
|
+
def batching?
|
13
|
+
Thread.current[:sidekiq_batch]
|
14
|
+
end
|
15
|
+
|
12
16
|
def push(item)
|
17
|
+
# 6160 we can't support both Sidekiq::Batch and transactions.
|
18
|
+
return @redis_client.push(item) if batching?
|
19
|
+
|
13
20
|
# pre-allocate the JID so we can return it immediately and
|
14
21
|
# save it to the database as part of the transaction.
|
15
22
|
item["jid"] ||= SecureRandom.hex(12)
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
@@ -15,11 +15,16 @@ module Sidekiq
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def halt(res)
|
18
|
-
throw :halt, [res, {
|
18
|
+
throw :halt, [res, {Rack::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, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
|
23
|
+
end
|
24
|
+
|
25
|
+
def reload_page
|
26
|
+
current_location = request.referer.gsub(request.base_url, "")
|
27
|
+
redirect current_location
|
23
28
|
end
|
24
29
|
|
25
30
|
def params
|
@@ -42,7 +47,8 @@ module Sidekiq
|
|
42
47
|
def erb(content, options = {})
|
43
48
|
if content.is_a? Symbol
|
44
49
|
unless respond_to?(:"_erb_#{content}")
|
45
|
-
|
50
|
+
views = options[:views] || Web.settings.views
|
51
|
+
src = ERB.new(File.read("#{views}/#{content}.erb")).src
|
46
52
|
WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
53
|
def _erb_#{content}
|
48
54
|
#{src}
|
@@ -68,7 +74,7 @@ module Sidekiq
|
|
68
74
|
end
|
69
75
|
|
70
76
|
def json(payload)
|
71
|
-
[200, {
|
77
|
+
[200, {Rack::CONTENT_TYPE => "application/json", Rack::CACHE_CONTROL => "private, no-store"}, [Sidekiq.dump_json(payload)]]
|
72
78
|
end
|
73
79
|
|
74
80
|
def initialize(env, block)
|
@@ -5,7 +5,7 @@ module Sidekiq
|
|
5
5
|
extend WebRouter
|
6
6
|
|
7
7
|
REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
|
8
|
-
|
8
|
+
CSP_HEADER_TEMPLATE = [
|
9
9
|
"default-src 'self' https: http:",
|
10
10
|
"child-src 'self'",
|
11
11
|
"connect-src 'self' https: http: wss: ws:",
|
@@ -15,11 +15,17 @@ module Sidekiq
|
|
15
15
|
"manifest-src 'self'",
|
16
16
|
"media-src 'self'",
|
17
17
|
"object-src 'none'",
|
18
|
-
"script-src 'self'
|
19
|
-
"style-src 'self' https: http: 'unsafe-inline'",
|
18
|
+
"script-src 'self' 'nonce-!placeholder!'",
|
19
|
+
"style-src 'self' https: http: 'unsafe-inline'", # TODO Nonce in 8.0
|
20
20
|
"worker-src 'self'",
|
21
21
|
"base-uri 'self'"
|
22
22
|
].join("; ").freeze
|
23
|
+
METRICS_PERIODS = {
|
24
|
+
"1h" => 60,
|
25
|
+
"2h" => 120,
|
26
|
+
"4h" => 240,
|
27
|
+
"8h" => 480
|
28
|
+
}
|
23
29
|
|
24
30
|
def initialize(klass)
|
25
31
|
@klass = klass
|
@@ -43,9 +49,9 @@ module Sidekiq
|
|
43
49
|
|
44
50
|
head "/" do
|
45
51
|
# HEAD / is the cheapest heartbeat possible,
|
46
|
-
# it hits Redis to ensure connectivity
|
47
|
-
|
48
|
-
""
|
52
|
+
# it hits Redis to ensure connectivity and returns
|
53
|
+
# the size of the default queue
|
54
|
+
Sidekiq.redis { |c| c.llen("queue:default") }.to_s
|
49
55
|
end
|
50
56
|
|
51
57
|
get "/" do
|
@@ -62,14 +68,20 @@ module Sidekiq
|
|
62
68
|
|
63
69
|
get "/metrics" do
|
64
70
|
q = Sidekiq::Metrics::Query.new
|
65
|
-
@
|
71
|
+
@period = h((params[:period] || "")[0..1])
|
72
|
+
@periods = METRICS_PERIODS
|
73
|
+
minutes = @periods.fetch(@period, @periods.values.first)
|
74
|
+
@query_result = q.top_jobs(minutes: minutes)
|
66
75
|
erb(:metrics)
|
67
76
|
end
|
68
77
|
|
69
78
|
get "/metrics/:name" do
|
70
79
|
@name = route_params[:name]
|
80
|
+
@period = h((params[:period] || "")[0..1])
|
71
81
|
q = Sidekiq::Metrics::Query.new
|
72
|
-
@
|
82
|
+
@periods = METRICS_PERIODS
|
83
|
+
minutes = @periods.fetch(@period, @periods.values.first)
|
84
|
+
@query_result = q.for_job(@name, minutes: minutes)
|
73
85
|
erb(:metrics_for_job)
|
74
86
|
end
|
75
87
|
|
@@ -82,11 +94,14 @@ module Sidekiq
|
|
82
94
|
|
83
95
|
post "/busy" do
|
84
96
|
if params["identity"]
|
85
|
-
|
86
|
-
|
87
|
-
|
97
|
+
pro = Sidekiq::ProcessSet[params["identity"]]
|
98
|
+
|
99
|
+
pro.quiet! if params["quiet"]
|
100
|
+
pro.stop! if params["stop"]
|
88
101
|
else
|
89
102
|
processes.each do |pro|
|
103
|
+
next if pro.embedded?
|
104
|
+
|
90
105
|
pro.quiet! if params["quiet"]
|
91
106
|
pro.stop! if params["stop"]
|
92
107
|
end
|
@@ -313,9 +328,87 @@ module Sidekiq
|
|
313
328
|
json Sidekiq::Stats.new.queues
|
314
329
|
end
|
315
330
|
|
331
|
+
########
|
332
|
+
# Filtering
|
333
|
+
|
334
|
+
get "/filter/metrics" do
|
335
|
+
redirect "#{root_path}metrics"
|
336
|
+
end
|
337
|
+
|
338
|
+
post "/filter/metrics" do
|
339
|
+
x = params[:substr]
|
340
|
+
q = Sidekiq::Metrics::Query.new
|
341
|
+
@period = h((params[:period] || "")[0..1])
|
342
|
+
@periods = METRICS_PERIODS
|
343
|
+
minutes = @periods.fetch(@period, @periods.values.first)
|
344
|
+
@query_result = q.top_jobs(minutes: minutes, class_filter: Regexp.new(Regexp.escape(x), Regexp::IGNORECASE))
|
345
|
+
|
346
|
+
erb :metrics
|
347
|
+
end
|
348
|
+
|
349
|
+
get "/filter/retries" do
|
350
|
+
x = params[:substr]
|
351
|
+
return redirect "#{root_path}retries" unless x && x != ""
|
352
|
+
|
353
|
+
@retries = search(Sidekiq::RetrySet.new, params[:substr])
|
354
|
+
erb :retries
|
355
|
+
end
|
356
|
+
|
357
|
+
post "/filter/retries" do
|
358
|
+
x = params[:substr]
|
359
|
+
return redirect "#{root_path}retries" unless x && x != ""
|
360
|
+
|
361
|
+
@retries = search(Sidekiq::RetrySet.new, params[:substr])
|
362
|
+
erb :retries
|
363
|
+
end
|
364
|
+
|
365
|
+
get "/filter/scheduled" do
|
366
|
+
x = params[:substr]
|
367
|
+
return redirect "#{root_path}scheduled" unless x && x != ""
|
368
|
+
|
369
|
+
@scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
|
370
|
+
erb :scheduled
|
371
|
+
end
|
372
|
+
|
373
|
+
post "/filter/scheduled" do
|
374
|
+
x = params[:substr]
|
375
|
+
return redirect "#{root_path}scheduled" unless x && x != ""
|
376
|
+
|
377
|
+
@scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
|
378
|
+
erb :scheduled
|
379
|
+
end
|
380
|
+
|
381
|
+
get "/filter/dead" do
|
382
|
+
x = params[:substr]
|
383
|
+
return redirect "#{root_path}morgue" unless x && x != ""
|
384
|
+
|
385
|
+
@dead = search(Sidekiq::DeadSet.new, params[:substr])
|
386
|
+
erb :morgue
|
387
|
+
end
|
388
|
+
|
389
|
+
post "/filter/dead" do
|
390
|
+
x = params[:substr]
|
391
|
+
return redirect "#{root_path}morgue" unless x && x != ""
|
392
|
+
|
393
|
+
@dead = search(Sidekiq::DeadSet.new, params[:substr])
|
394
|
+
erb :morgue
|
395
|
+
end
|
396
|
+
|
397
|
+
post "/change_locale" do
|
398
|
+
locale = params["locale"]
|
399
|
+
|
400
|
+
match = available_locales.find { |available|
|
401
|
+
locale == available
|
402
|
+
}
|
403
|
+
|
404
|
+
session[:locale] = match if match
|
405
|
+
|
406
|
+
reload_page
|
407
|
+
end
|
408
|
+
|
316
409
|
def call(env)
|
317
410
|
action = self.class.match(env)
|
318
|
-
return [404, {
|
411
|
+
return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
|
319
412
|
|
320
413
|
app = @klass
|
321
414
|
resp = catch(:halt) do
|
@@ -332,16 +425,20 @@ module Sidekiq
|
|
332
425
|
else
|
333
426
|
# rendered content goes here
|
334
427
|
headers = {
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
428
|
+
Rack::CONTENT_TYPE => "text/html",
|
429
|
+
Rack::CACHE_CONTROL => "private, no-store",
|
430
|
+
Web::CONTENT_LANGUAGE => action.locale,
|
431
|
+
Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE)
|
339
432
|
}
|
340
433
|
# we'll let Rack calculate Content-Length for us.
|
341
434
|
[200, headers, [resp]]
|
342
435
|
end
|
343
436
|
end
|
344
437
|
|
438
|
+
def process_csp(env, input)
|
439
|
+
input.gsub("!placeholder!", env[:csp_nonce])
|
440
|
+
end
|
441
|
+
|
345
442
|
def self.helpers(mod = nil, &block)
|
346
443
|
if block
|
347
444
|
WebAction.class_eval(&block)
|
@@ -27,7 +27,6 @@
|
|
27
27
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
28
28
|
|
29
29
|
require "securerandom"
|
30
|
-
require "base64"
|
31
30
|
require "rack/request"
|
32
31
|
|
33
32
|
module Sidekiq
|
@@ -57,12 +56,12 @@ module Sidekiq
|
|
57
56
|
end
|
58
57
|
|
59
58
|
def logger(env)
|
60
|
-
@logger ||=
|
59
|
+
@logger ||= env["rack.logger"] || ::Logger.new(env["rack.errors"])
|
61
60
|
end
|
62
61
|
|
63
62
|
def deny(env)
|
64
63
|
logger(env).warn "attack prevented by #{self.class}"
|
65
|
-
[403, {
|
64
|
+
[403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
|
66
65
|
end
|
67
66
|
|
68
67
|
def session(env)
|
@@ -116,7 +115,7 @@ module Sidekiq
|
|
116
115
|
sess = session(env)
|
117
116
|
localtoken = sess[:csrf]
|
118
117
|
|
119
|
-
# Checks that Rack::Session::Cookie
|
118
|
+
# Checks that Rack::Session::Cookie actually contains the csrf token
|
120
119
|
return false if localtoken.nil?
|
121
120
|
|
122
121
|
# Rotate the session token after every use
|
@@ -143,7 +142,7 @@ module Sidekiq
|
|
143
142
|
one_time_pad = SecureRandom.random_bytes(token.length)
|
144
143
|
encrypted_token = xor_byte_strings(one_time_pad, token)
|
145
144
|
masked_token = one_time_pad + encrypted_token
|
146
|
-
|
145
|
+
encode_token(masked_token)
|
147
146
|
end
|
148
147
|
|
149
148
|
# Essentially the inverse of +mask_token+.
|
@@ -168,8 +167,12 @@ module Sidekiq
|
|
168
167
|
::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
|
169
168
|
end
|
170
169
|
|
170
|
+
def encode_token(token)
|
171
|
+
[token].pack("m0").tr("+/", "-_")
|
172
|
+
end
|
173
|
+
|
171
174
|
def decode_token(token)
|
172
|
-
|
175
|
+
token.tr("-_", "+/").unpack1("m0")
|
173
176
|
end
|
174
177
|
|
175
178
|
def xor_byte_strings(s1, s2)
|