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.
- checksums.yaml +4 -4
- data/Changes.md +51 -0
- data/README.md +1 -1
- data/lib/generators/sidekiq/job_generator.rb +2 -0
- data/lib/sidekiq/api.rb +10 -4
- data/lib/sidekiq/capsule.rb +5 -0
- data/lib/sidekiq/cli.rb +1 -0
- data/lib/sidekiq/client.rb +4 -1
- data/lib/sidekiq/config.rb +7 -1
- data/lib/sidekiq/deploy.rb +2 -0
- data/lib/sidekiq/embedded.rb +2 -0
- data/lib/sidekiq/fetch.rb +1 -1
- data/lib/sidekiq/iterable_job.rb +55 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -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 +13 -2
- data/lib/sidekiq/job_logger.rb +22 -11
- data/lib/sidekiq/job_retry.rb +6 -1
- data/lib/sidekiq/job_util.rb +2 -0
- data/lib/sidekiq/metrics/query.rb +2 -0
- data/lib/sidekiq/metrics/shared.rb +2 -0
- data/lib/sidekiq/metrics/tracking.rb +13 -5
- data/lib/sidekiq/middleware/current_attributes.rb +29 -11
- data/lib/sidekiq/middleware/modules.rb +2 -0
- data/lib/sidekiq/monitor.rb +2 -1
- data/lib/sidekiq/processor.rb +11 -1
- data/lib/sidekiq/redis_client_adapter.rb +8 -5
- data/lib/sidekiq/redis_connection.rb +33 -2
- data/lib/sidekiq/ring_buffer.rb +2 -0
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/version.rb +1 -1
- data/lib/sidekiq/web/action.rb +2 -1
- data/lib/sidekiq/web/application.rb +9 -4
- data/lib/sidekiq/web/helpers.rb +53 -7
- data/lib/sidekiq/web.rb +48 -1
- data/lib/sidekiq.rb +2 -1
- data/sidekiq.gemspec +2 -1
- data/web/assets/javascripts/application.js +6 -1
- data/web/assets/javascripts/dashboard-charts.js +22 -12
- data/web/assets/javascripts/dashboard.js +1 -1
- data/web/assets/stylesheets/application.css +11 -3
- data/web/locales/tr.yml +101 -0
- data/web/views/dashboard.erb +6 -6
- data/web/views/layout.erb +6 -6
- data/web/views/metrics.erb +4 -4
- data/web/views/metrics_for_job.erb +4 -4
- 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
|
-
|
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
|
data/lib/sidekiq/job_logger.rb
CHANGED
@@ -2,23 +2,34 @@
|
|
2
2
|
|
3
3
|
module Sidekiq
|
4
4
|
class JobLogger
|
5
|
-
def initialize(
|
6
|
-
@
|
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
|
-
|
11
|
-
@logger.info("start")
|
17
|
+
return yield if skip_default_logging?
|
12
18
|
|
13
|
-
|
19
|
+
begin
|
20
|
+
start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
21
|
+
@logger.info("start")
|
14
22
|
|
15
|
-
|
16
|
-
@logger.info("done")
|
17
|
-
rescue Exception
|
18
|
-
Sidekiq::Context.add(:elapsed, elapsed(start))
|
19
|
-
@logger.info("fail")
|
23
|
+
yield
|
20
24
|
|
21
|
-
|
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)
|
data/lib/sidekiq/job_retry.rb
CHANGED
@@ -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
|
137
|
+
raise Handled
|
133
138
|
end
|
134
139
|
|
135
140
|
private
|
data/lib/sidekiq/job_util.rb
CHANGED
@@ -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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
51
|
+
klass_attrs = {}
|
50
52
|
|
51
53
|
@cattrs.each do |(key, strklass)|
|
52
|
-
|
53
|
-
constklass = strklass.constantize
|
54
|
-
cattrs_to_reset << constklass
|
54
|
+
next unless job.has_key?(key)
|
55
55
|
|
56
|
-
|
57
|
-
constklass.public_send(:"#{attribute}=", value)
|
58
|
-
end
|
59
|
-
end
|
56
|
+
klass_attrs[strklass.constantize] = job[key]
|
60
57
|
end
|
61
58
|
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
|
data/lib/sidekiq/monitor.rb
CHANGED
@@ -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
|
data/lib/sidekiq/processor.rb
CHANGED
@@ -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(
|
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
|
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
|
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
|
-
|
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
|
data/lib/sidekiq/ring_buffer.rb
CHANGED
data/lib/sidekiq/systemd.rb
CHANGED
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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'
|
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 =>
|
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)
|