sidekiq 7.2.4 → 7.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +43 -0
  3. data/README.md +1 -1
  4. data/lib/generators/sidekiq/job_generator.rb +2 -0
  5. data/lib/sidekiq/api.rb +10 -4
  6. data/lib/sidekiq/capsule.rb +5 -0
  7. data/lib/sidekiq/cli.rb +1 -0
  8. data/lib/sidekiq/client.rb +4 -1
  9. data/lib/sidekiq/config.rb +7 -1
  10. data/lib/sidekiq/deploy.rb +2 -0
  11. data/lib/sidekiq/embedded.rb +2 -0
  12. data/lib/sidekiq/fetch.rb +1 -1
  13. data/lib/sidekiq/iterable_job.rb +55 -0
  14. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  15. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  16. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  17. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  18. data/lib/sidekiq/job/iterable.rb +231 -0
  19. data/lib/sidekiq/job.rb +13 -2
  20. data/lib/sidekiq/job_logger.rb +22 -11
  21. data/lib/sidekiq/job_retry.rb +6 -1
  22. data/lib/sidekiq/job_util.rb +2 -0
  23. data/lib/sidekiq/metrics/query.rb +2 -0
  24. data/lib/sidekiq/metrics/shared.rb +2 -0
  25. data/lib/sidekiq/metrics/tracking.rb +13 -5
  26. data/lib/sidekiq/middleware/current_attributes.rb +29 -11
  27. data/lib/sidekiq/middleware/modules.rb +2 -0
  28. data/lib/sidekiq/monitor.rb +2 -1
  29. data/lib/sidekiq/processor.rb +11 -1
  30. data/lib/sidekiq/redis_client_adapter.rb +8 -5
  31. data/lib/sidekiq/redis_connection.rb +33 -2
  32. data/lib/sidekiq/ring_buffer.rb +2 -0
  33. data/lib/sidekiq/systemd.rb +2 -0
  34. data/lib/sidekiq/version.rb +1 -1
  35. data/lib/sidekiq/web/action.rb +2 -1
  36. data/lib/sidekiq/web/application.rb +9 -4
  37. data/lib/sidekiq/web/helpers.rb +53 -7
  38. data/lib/sidekiq/web.rb +48 -1
  39. data/lib/sidekiq.rb +2 -1
  40. data/sidekiq.gemspec +2 -1
  41. data/web/assets/javascripts/application.js +6 -1
  42. data/web/assets/javascripts/dashboard-charts.js +22 -12
  43. data/web/assets/javascripts/dashboard.js +1 -1
  44. data/web/assets/stylesheets/application.css +13 -1
  45. data/web/locales/tr.yml +101 -0
  46. data/web/views/dashboard.erb +6 -6
  47. data/web/views/layout.erb +6 -6
  48. data/web/views/metrics.erb +4 -4
  49. data/web/views/metrics_for_job.erb +4 -4
  50. metadata +26 -5
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "iterable/enumerators"
4
+
5
+ module Sidekiq
6
+ module Job
7
+ class Interrupted < ::RuntimeError; end
8
+
9
+ module Iterable
10
+ include Enumerators
11
+
12
+ # @api private
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ # @api private
18
+ module ClassMethods
19
+ def method_added(method_name)
20
+ raise "#{self} is an iterable job and must not define #perform" if method_name == :perform
21
+ super
22
+ end
23
+ end
24
+
25
+ # @api private
26
+ def initialize
27
+ super
28
+
29
+ @_executions = 0
30
+ @_cursor = nil
31
+ @_start_time = nil
32
+ @_runtime = 0
33
+ end
34
+
35
+ # A hook to override that will be called when the job starts iterating.
36
+ #
37
+ # It is called only once, for the first time.
38
+ #
39
+ def on_start
40
+ end
41
+
42
+ # A hook to override that will be called around each iteration.
43
+ #
44
+ # Can be useful for some metrics collection, performance tracking etc.
45
+ #
46
+ def around_iteration
47
+ yield
48
+ end
49
+
50
+ # A hook to override that will be called when the job resumes iterating.
51
+ #
52
+ def on_resume
53
+ end
54
+
55
+ # A hook to override that will be called each time the job is interrupted.
56
+ #
57
+ # This can be due to interruption or sidekiq stopping.
58
+ #
59
+ def on_stop
60
+ end
61
+
62
+ # A hook to override that will be called when the job finished iterating.
63
+ #
64
+ def on_complete
65
+ end
66
+
67
+ # The enumerator to be iterated over.
68
+ #
69
+ # @return [Enumerator]
70
+ #
71
+ # @raise [NotImplementedError] with a message advising subclasses to
72
+ # implement an override for this method.
73
+ #
74
+ def build_enumerator(*)
75
+ raise NotImplementedError, "#{self.class.name} must implement a '#build_enumerator' method"
76
+ end
77
+
78
+ # The action to be performed on each item from the enumerator.
79
+ #
80
+ # @return [void]
81
+ #
82
+ # @raise [NotImplementedError] with a message advising subclasses to
83
+ # implement an override for this method.
84
+ #
85
+ def each_iteration(*)
86
+ raise NotImplementedError, "#{self.class.name} must implement an '#each_iteration' method"
87
+ end
88
+
89
+ def iteration_key
90
+ "it-#{jid}"
91
+ end
92
+
93
+ # @api private
94
+ def perform(*arguments)
95
+ fetch_previous_iteration_state
96
+
97
+ @_executions += 1
98
+ @_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
99
+
100
+ enumerator = build_enumerator(*arguments, cursor: @_cursor)
101
+ unless enumerator
102
+ logger.info("'#build_enumerator' returned nil, skipping the job.")
103
+ return
104
+ end
105
+
106
+ assert_enumerator!(enumerator)
107
+
108
+ if @_executions == 1
109
+ on_start
110
+ else
111
+ on_resume
112
+ end
113
+
114
+ completed = catch(:abort) do
115
+ iterate_with_enumerator(enumerator, arguments)
116
+ end
117
+
118
+ on_stop
119
+ completed = handle_completed(completed)
120
+
121
+ if completed
122
+ on_complete
123
+ cleanup
124
+ else
125
+ reenqueue_iteration_job
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def fetch_previous_iteration_state
132
+ state = Sidekiq.redis { |conn| conn.hgetall(iteration_key) }
133
+
134
+ unless state.empty?
135
+ @_executions = state["ex"].to_i
136
+ @_cursor = Sidekiq.load_json(state["c"])
137
+ @_runtime = state["rt"].to_f
138
+ end
139
+ end
140
+
141
+ STATE_FLUSH_INTERVAL = 5 # seconds
142
+ # we need to keep the state around as long as the job
143
+ # might be retrying
144
+ STATE_TTL = 30 * 24 * 60 * 60 # one month
145
+
146
+ def iterate_with_enumerator(enumerator, arguments)
147
+ found_record = false
148
+ state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
149
+
150
+ enumerator.each do |object, cursor|
151
+ found_record = true
152
+ @_cursor = cursor
153
+
154
+ is_interrupted = interrupted?
155
+ if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
156
+ flush_state
157
+ state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
158
+ end
159
+
160
+ return false if is_interrupted
161
+
162
+ around_iteration do
163
+ each_iteration(object, *arguments)
164
+ end
165
+ end
166
+
167
+ logger.debug("Enumerator found nothing to iterate!") unless found_record
168
+ true
169
+ ensure
170
+ @_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
171
+ end
172
+
173
+ def reenqueue_iteration_job
174
+ flush_state
175
+ logger.debug { "Interrupting job (cursor=#{@_cursor.inspect})" }
176
+
177
+ raise Interrupted
178
+ end
179
+
180
+ def assert_enumerator!(enum)
181
+ unless enum.is_a?(Enumerator)
182
+ raise ArgumentError, <<~MSG
183
+ #build_enumerator must return an Enumerator, but returned #{enum.class}.
184
+ Example:
185
+ def build_enumerator(params, cursor:)
186
+ active_record_records_enumerator(
187
+ Shop.find(params["shop_id"]).products,
188
+ cursor: cursor
189
+ )
190
+ end
191
+ MSG
192
+ end
193
+ end
194
+
195
+ def flush_state
196
+ key = iteration_key
197
+ state = {
198
+ "ex" => @_executions,
199
+ "c" => Sidekiq.dump_json(@_cursor),
200
+ "rt" => @_runtime
201
+ }
202
+
203
+ Sidekiq.redis do |conn|
204
+ conn.multi do |pipe|
205
+ pipe.hset(key, state)
206
+ pipe.expire(key, STATE_TTL)
207
+ end
208
+ end
209
+ end
210
+
211
+ def cleanup
212
+ logger.debug {
213
+ format("Completed iteration. executions=%d runtime=%.3f", @_executions, @_runtime)
214
+ }
215
+ Sidekiq.redis { |conn| conn.unlink(iteration_key) }
216
+ end
217
+
218
+ def handle_completed(completed)
219
+ case completed
220
+ when nil, # someone aborted the job but wants to call the on_complete callback
221
+ true
222
+ true
223
+ when false
224
+ false
225
+ else
226
+ raise "Unexpected thrown value: #{completed.inspect}"
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
data/lib/sidekiq/job.rb CHANGED
@@ -69,7 +69,11 @@ module Sidekiq
69
69
  # In practice, any option is allowed. This is the main mechanism to configure the
70
70
  # options for a specific job.
71
71
  def sidekiq_options(opts = {})
72
- opts = opts.transform_keys(&:to_s) # stringify
72
+ # stringify 2 levels of keys
73
+ opts = opts.to_h do |k, v|
74
+ [k.to_s, (Hash === v) ? v.transform_keys(&:to_s) : v]
75
+ end
76
+
73
77
  self.sidekiq_options_hash = get_sidekiq_options.merge(opts)
74
78
  end
75
79
 
@@ -155,6 +159,9 @@ module Sidekiq
155
159
 
156
160
  attr_accessor :jid
157
161
 
162
+ # This attribute is implementation-specific and not a public API
163
+ attr_accessor :_context
164
+
158
165
  def self.included(base)
159
166
  raise ArgumentError, "Sidekiq::Job cannot be included in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
160
167
 
@@ -166,6 +173,10 @@ module Sidekiq
166
173
  Sidekiq.logger
167
174
  end
168
175
 
176
+ def interrupted?
177
+ @_context&.stopping?
178
+ end
179
+
169
180
  # This helper class encapsulates the set options for `set`, e.g.
170
181
  #
171
182
  # SomeJob.set(queue: 'foo').perform_async(....)
@@ -366,7 +377,7 @@ module Sidekiq
366
377
 
367
378
  def build_client # :nodoc:
368
379
  pool = Thread.current[:sidekiq_redis_pool] || get_sidekiq_options["pool"] || Sidekiq.default_configuration.redis_pool
369
- client_class = get_sidekiq_options["client_class"] || Sidekiq::Client
380
+ client_class = Thread.current[:sidekiq_client_class] || get_sidekiq_options["client_class"] || Sidekiq::Client
370
381
  client_class.new(pool: pool)
371
382
  end
372
383
  end
@@ -2,23 +2,34 @@
2
2
 
3
3
  module Sidekiq
4
4
  class JobLogger
5
- def initialize(logger)
6
- @logger = logger
5
+ def initialize(config)
6
+ @config = config
7
+ @logger = @config.logger
8
+ end
9
+
10
+ # If true we won't do any job logging out of the box.
11
+ # The user is responsible for any logging.
12
+ def skip_default_logging?
13
+ @config[:skip_default_job_logging]
7
14
  end
8
15
 
9
16
  def call(item, queue)
10
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
11
- @logger.info("start")
17
+ return yield if skip_default_logging?
12
18
 
13
- yield
19
+ begin
20
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
21
+ @logger.info("start")
14
22
 
15
- Sidekiq::Context.add(:elapsed, elapsed(start))
16
- @logger.info("done")
17
- rescue Exception
18
- Sidekiq::Context.add(:elapsed, elapsed(start))
19
- @logger.info("fail")
23
+ yield
20
24
 
21
- raise
25
+ Sidekiq::Context.add(:elapsed, elapsed(start))
26
+ @logger.info("done")
27
+ rescue Exception
28
+ Sidekiq::Context.add(:elapsed, elapsed(start))
29
+ @logger.info("fail")
30
+
31
+ raise
32
+ end
22
33
  end
23
34
 
24
35
  def prepare(job_hash, &block)
@@ -59,8 +59,13 @@ module Sidekiq
59
59
  # end
60
60
  #
61
61
  class JobRetry
62
+ # Handled means the job failed but has been dealt with
63
+ # (by creating a retry, rescheduling it, etc). It still
64
+ # needs to be logged and dispatched to error_handlers.
62
65
  class Handled < ::RuntimeError; end
63
66
 
67
+ # Skip means the job failed but Sidekiq does not need to
68
+ # create a retry, log it or send to error_handlers.
64
69
  class Skip < Handled; end
65
70
 
66
71
  include Sidekiq::Component
@@ -129,7 +134,7 @@ module Sidekiq
129
134
  process_retry(jobinst, msg, queue, e)
130
135
  # We've handled this error associated with this job, don't
131
136
  # need to handle it at the global level
132
- raise Skip
137
+ raise Handled
133
138
  end
134
139
 
135
140
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
  require "time"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq"
2
4
  require "date"
3
5
  require "set"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "concurrent"
2
4
 
3
5
  module Sidekiq
@@ -31,11 +31,11 @@ module Sidekiq
31
31
  # We don't track time for failed jobs as they can have very unpredictable
32
32
  # execution times. more important to know average time for successful jobs so we
33
33
  # can better recognize when a perf regression is introduced.
34
- @lock.synchronize {
35
- @grams[klass].record_time(time_ms)
36
- @jobs["#{klass}|ms"] += time_ms
37
- @totals["ms"] += time_ms
38
- }
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
39
39
  rescue Exception
40
40
  @lock.synchronize {
41
41
  @jobs["#{klass}|f"] += 1
@@ -100,6 +100,14 @@ module Sidekiq
100
100
 
101
101
  private
102
102
 
103
+ def track_time(klass, time_ms)
104
+ @lock.synchronize {
105
+ @grams[klass].record_time(time_ms)
106
+ @jobs["#{klass}|ms"] += time_ms
107
+ @totals["ms"] += time_ms
108
+ }
109
+ end
110
+
103
111
  def reset
104
112
  @lock.synchronize {
105
113
  array = [@totals, @jobs, @grams]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/current_attributes"
2
4
 
3
5
  module Sidekiq
@@ -46,22 +48,38 @@ module Sidekiq
46
48
  end
47
49
 
48
50
  def call(_, job, _, &block)
49
- cattrs_to_reset = []
51
+ klass_attrs = {}
50
52
 
51
53
  @cattrs.each do |(key, strklass)|
52
- if job.has_key?(key)
53
- constklass = strklass.constantize
54
- cattrs_to_reset << constklass
54
+ next unless job.has_key?(key)
55
55
 
56
- job[key].each do |(attribute, value)|
57
- constklass.public_send(:"#{attribute}=", value)
58
- end
59
- end
56
+ klass_attrs[strklass.constantize] = job[key]
60
57
  end
61
58
 
62
- yield
63
- ensure
64
- cattrs_to_reset.each(&:reset)
59
+ wrap(klass_attrs.to_a, &block)
60
+ end
61
+
62
+ private
63
+
64
+ def wrap(klass_attrs, &block)
65
+ klass, attrs = klass_attrs.shift
66
+ return block.call unless klass
67
+
68
+ retried = false
69
+
70
+ begin
71
+ klass.set(attrs) do
72
+ wrap(klass_attrs, &block)
73
+ end
74
+ rescue NoMethodError
75
+ raise if retried
76
+
77
+ # It is possible that the `CurrentAttributes` definition
78
+ # was changed before the job started processing.
79
+ attrs = attrs.select { |attr| klass.respond_to?(attr) }
80
+ retried = true
81
+ retry
82
+ end
65
83
  end
66
84
  end
67
85
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  # Server-side middleware must import this Module in order
3
5
  # to get access to server resources during `call`.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "fileutils"
4
5
  require "sidekiq/api"
@@ -98,7 +99,7 @@ class Sidekiq::Monitor
98
99
  pad = opts[:pad] || 0
99
100
  max_length = opts[:max_length] || (80 - pad)
100
101
  out = []
101
- line = ""
102
+ line = +""
102
103
  values.each do |value|
103
104
  if (line.length + value.length) > max_length
104
105
  out << line
@@ -36,7 +36,7 @@ module Sidekiq
36
36
  @job = nil
37
37
  @thread = nil
38
38
  @reloader = Sidekiq.default_configuration[:reloader]
39
- @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(logger)
39
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
40
40
  @retrier = Sidekiq::JobRetry.new(capsule)
41
41
  end
42
42
 
@@ -58,6 +58,10 @@ module Sidekiq
58
58
  @thread.value if wait
59
59
  end
60
60
 
61
+ def stopping?
62
+ @done
63
+ end
64
+
61
65
  def start
62
66
  @thread ||= safe_thread("#{config.name}/processor", &method(:run))
63
67
  end
@@ -136,6 +140,7 @@ module Sidekiq
136
140
  klass = Object.const_get(job_hash["class"])
137
141
  inst = klass.new
138
142
  inst.jid = job_hash["jid"]
143
+ inst._context = self
139
144
  @retrier.local(inst, jobstr, queue) do
140
145
  yield inst
141
146
  end
@@ -185,6 +190,11 @@ module Sidekiq
185
190
  # Had to force kill this job because it didn't finish
186
191
  # within the timeout. Don't acknowledge the work since
187
192
  # we didn't properly finish it.
193
+ rescue Sidekiq::JobRetry::Skip => s
194
+ # Skip means we handled this error elsewhere. We don't
195
+ # need to log or report the error.
196
+ ack = true
197
+ raise s
188
198
  rescue Sidekiq::JobRetry::Handled => h
189
199
  # this is the common case: job raised error and Sidekiq::JobRetry::Handled
190
200
  # signals that we created a retry successfully. We can acknowledge the job.
@@ -64,6 +64,13 @@ module Sidekiq
64
64
  opts = client_opts(options)
65
65
  @config = if opts.key?(:sentinels)
66
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)
67
74
  else
68
75
  RedisClient.config(**opts)
69
76
  end
@@ -90,13 +97,9 @@ module Sidekiq
90
97
  opts.delete(:network_timeout)
91
98
  end
92
99
 
93
- if opts[:driver]
94
- opts[:driver] = opts[:driver].to_sym
95
- end
96
-
97
100
  opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
98
101
  opts[:role] = opts[:role].to_sym if opts.key?(:role)
99
- opts.delete(:url) if opts.key?(:sentinels)
102
+ opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
100
103
 
101
104
  # Issue #3303, redis-rb will silently retry an operation.
102
105
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -8,17 +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
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
+
18
22
  size = symbolized_options.delete(:size) || 5
19
23
  pool_timeout = symbolized_options.delete(:pool_timeout) || 1
20
24
  pool_name = symbolized_options.delete(:pool_name)
21
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
+
22
33
  redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
23
34
  ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
24
35
  redis_config.new_client
@@ -27,6 +38,19 @@ module Sidekiq
27
38
 
28
39
  private
29
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
+
30
54
  def scrub(options)
31
55
  redacted = "REDACTED"
32
56
 
@@ -42,7 +66,14 @@ module Sidekiq
42
66
  scrubbed_options[:password] = redacted if scrubbed_options[:password]
43
67
  scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
44
68
  scrubbed_options[:sentinels]&.each do |sentinel|
45
- sentinel[:password] = redacted if sentinel[:password]
69
+ if sentinel.is_a?(String)
70
+ if (uri = URI(sentinel)) && uri.password
71
+ uri.password = redacted
72
+ sentinel.replace(uri.to_s)
73
+ end
74
+ elsif sentinel[:password]
75
+ sentinel[:password] = redacted
76
+ end
46
77
  end
47
78
  scrubbed_options
48
79
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  module Sidekiq
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Sidekiq's systemd integration allows Sidekiq to inform systemd:
3
5
  # 1. when it has successfully started
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.2.4"
4
+ VERSION = "7.3.1"
5
5
  MAJOR = 7
6
6
  end
@@ -47,7 +47,8 @@ module Sidekiq
47
47
  def erb(content, options = {})
48
48
  if content.is_a? Symbol
49
49
  unless respond_to?(:"_erb_#{content}")
50
- 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
51
52
  WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
52
53
  def _erb_#{content}
53
54
  #{src}
@@ -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,8 +15,8 @@ module Sidekiq
15
15
  "manifest-src 'self'",
16
16
  "media-src 'self'",
17
17
  "object-src 'none'",
18
- "script-src 'self' https: http:",
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
@@ -428,13 +428,18 @@ module Sidekiq
428
428
  Rack::CONTENT_TYPE => "text/html",
429
429
  Rack::CACHE_CONTROL => "private, no-store",
430
430
  Web::CONTENT_LANGUAGE => action.locale,
431
- Web::CONTENT_SECURITY_POLICY => CSP_HEADER
431
+ Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE),
432
+ Web::X_CONTENT_TYPE_OPTIONS => "nosniff"
432
433
  }
433
434
  # we'll let Rack calculate Content-Length for us.
434
435
  [200, headers, [resp]]
435
436
  end
436
437
  end
437
438
 
439
+ def process_csp(env, input)
440
+ input.gsub("!placeholder!", env[:csp_nonce])
441
+ end
442
+
438
443
  def self.helpers(mod = nil, &block)
439
444
  if block
440
445
  WebAction.class_eval(&block)