sidekiq 7.2.4 → 7.3.2

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +51 -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 +11 -3
  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.2"
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)