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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +261 -13
  3. data/README.md +34 -27
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +204 -109
  6. data/bin/sidekiqmon +3 -0
  7. data/lib/sidekiq/api.rb +151 -23
  8. data/lib/sidekiq/capsule.rb +20 -0
  9. data/lib/sidekiq/cli.rb +9 -4
  10. data/lib/sidekiq/client.rb +40 -24
  11. data/lib/sidekiq/component.rb +3 -1
  12. data/lib/sidekiq/config.rb +32 -12
  13. data/lib/sidekiq/deploy.rb +5 -5
  14. data/lib/sidekiq/embedded.rb +3 -3
  15. data/lib/sidekiq/fetch.rb +3 -5
  16. data/lib/sidekiq/iterable_job.rb +53 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +22 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  19. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  20. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  21. data/lib/sidekiq/job/iterable.rb +231 -0
  22. data/lib/sidekiq/job.rb +17 -10
  23. data/lib/sidekiq/job_logger.rb +24 -11
  24. data/lib/sidekiq/job_retry.rb +34 -11
  25. data/lib/sidekiq/job_util.rb +51 -15
  26. data/lib/sidekiq/launcher.rb +38 -22
  27. data/lib/sidekiq/logger.rb +1 -1
  28. data/lib/sidekiq/metrics/query.rb +6 -3
  29. data/lib/sidekiq/metrics/shared.rb +4 -4
  30. data/lib/sidekiq/metrics/tracking.rb +9 -3
  31. data/lib/sidekiq/middleware/chain.rb +12 -9
  32. data/lib/sidekiq/middleware/current_attributes.rb +70 -17
  33. data/lib/sidekiq/monitor.rb +17 -4
  34. data/lib/sidekiq/paginator.rb +4 -4
  35. data/lib/sidekiq/processor.rb +41 -27
  36. data/lib/sidekiq/rails.rb +18 -8
  37. data/lib/sidekiq/redis_client_adapter.rb +31 -35
  38. data/lib/sidekiq/redis_connection.rb +29 -7
  39. data/lib/sidekiq/scheduled.rb +4 -4
  40. data/lib/sidekiq/testing.rb +27 -8
  41. data/lib/sidekiq/transaction_aware_client.rb +7 -0
  42. data/lib/sidekiq/version.rb +1 -1
  43. data/lib/sidekiq/web/action.rb +10 -4
  44. data/lib/sidekiq/web/application.rb +113 -16
  45. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  46. data/lib/sidekiq/web/helpers.rb +104 -33
  47. data/lib/sidekiq/web.rb +63 -2
  48. data/lib/sidekiq.rb +2 -1
  49. data/sidekiq.gemspec +8 -29
  50. data/web/assets/javascripts/application.js +45 -0
  51. data/web/assets/javascripts/dashboard-charts.js +38 -12
  52. data/web/assets/javascripts/dashboard.js +8 -10
  53. data/web/assets/javascripts/metrics.js +64 -2
  54. data/web/assets/stylesheets/application-dark.css +4 -0
  55. data/web/assets/stylesheets/application-rtl.css +10 -0
  56. data/web/assets/stylesheets/application.css +38 -4
  57. data/web/locales/da.yml +11 -4
  58. data/web/locales/en.yml +2 -0
  59. data/web/locales/fr.yml +14 -0
  60. data/web/locales/gd.yml +99 -0
  61. data/web/locales/ja.yml +3 -1
  62. data/web/locales/pt-br.yml +20 -0
  63. data/web/locales/tr.yml +101 -0
  64. data/web/locales/zh-cn.yml +20 -19
  65. data/web/views/_footer.erb +14 -2
  66. data/web/views/_job_info.erb +18 -2
  67. data/web/views/_metrics_period_select.erb +12 -0
  68. data/web/views/_paging.erb +2 -0
  69. data/web/views/_poll_link.erb +1 -1
  70. data/web/views/_summary.erb +7 -7
  71. data/web/views/busy.erb +46 -35
  72. data/web/views/dashboard.erb +25 -35
  73. data/web/views/filtering.erb +7 -0
  74. data/web/views/layout.erb +6 -6
  75. data/web/views/metrics.erb +42 -31
  76. data/web/views/metrics_for_job.erb +41 -51
  77. data/web/views/morgue.erb +5 -9
  78. data/web/views/queue.erb +10 -14
  79. data/web/views/queues.erb +9 -3
  80. data/web/views/retries.erb +5 -9
  81. data/web/views/scheduled.erb +12 -13
  82. 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
- # underscore methods are not official API
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 isn't supported by redis-client. " \
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.delete(:url) if opts.key?(:sentinels)
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.transform_keys(&:to_sym)
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
- scrubbed_options[:password] = redacted
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
@@ -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("zrangebyscore", key, "-inf", now, "limit", 0, 1)
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.evalsha(@lua_zpopbyscore_sha, keys, argv)
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 artifical boost.
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", nx: true, ex: 60) }
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|
@@ -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 :__test_mode
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
- current_mode = __test_mode
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.__test_mode = current_mode
25
+ self.__local_test_mode = nil
19
26
  end
20
27
  else
21
- self.__test_mode = mode
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 raw_push(payloads)
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 dervied from a filter of the total
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)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.0.0"
4
+ VERSION = "7.3.0"
5
5
  MAJOR = 7
6
6
  end
@@ -15,11 +15,16 @@ module Sidekiq
15
15
  end
16
16
 
17
17
  def halt(res)
18
- throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
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, {"location" => "#{request.base_url}#{location}"}, []]
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
- src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
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, {"content-type" => "application/json", "cache-control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
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
- CSP_HEADER = [
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' https: http: 'unsafe-inline'",
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
- Sidekiq.redis { |c| c.llen("queue:default") }
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
- @query_result = q.top_jobs
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
- @query_result = q.for_job(@name)
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
- p = Sidekiq::Process.new("identity" => params["identity"])
86
- p.quiet! if params["quiet"]
87
- p.stop! if params["stop"]
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, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
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
- "content-type" => "text/html",
336
- "cache-control" => "private, no-store",
337
- "content-language" => action.locale,
338
- "content-security-policy" => CSP_HEADER
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 ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
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, {"Content-Type" => "text/plain"}, ["Forbidden"]]
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 actualy contains the csrf toekn
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
- Base64.urlsafe_encode64(masked_token)
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
- Base64.urlsafe_decode64(token)
175
+ token.tr("-_", "+/").unpack1("m0")
173
176
  end
174
177
 
175
178
  def xor_byte_strings(s1, s2)