sidekiq 7.3.9 → 8.1.7
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 +225 -0
- data/README.md +34 -14
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/sidekiqload +12 -10
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +107 -58
- data/lib/generators/sidekiq/job_generator.rb +15 -3
- data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
- data/lib/sidekiq/api.rb +310 -83
- data/lib/sidekiq/capsule.rb +10 -7
- data/lib/sidekiq/cli.rb +31 -24
- data/lib/sidekiq/client.rb +36 -18
- data/lib/sidekiq/component.rb +45 -3
- data/lib/sidekiq/config.rb +35 -30
- data/lib/sidekiq/embedded.rb +2 -1
- data/lib/sidekiq/fetch.rb +1 -0
- data/lib/sidekiq/iterable_job.rb +1 -0
- data/lib/sidekiq/job/iterable.rb +46 -18
- data/lib/sidekiq/job.rb +18 -3
- data/lib/sidekiq/job_logger.rb +8 -6
- data/lib/sidekiq/job_retry.rb +45 -18
- data/lib/sidekiq/job_util.rb +5 -1
- data/lib/sidekiq/launcher.rb +22 -12
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +25 -69
- data/lib/sidekiq/manager.rb +2 -2
- data/lib/sidekiq/metrics/query.rb +77 -46
- data/lib/sidekiq/metrics/shared.rb +8 -5
- data/lib/sidekiq/metrics/tracking.rb +12 -7
- data/lib/sidekiq/middleware/current_attributes.rb +11 -19
- data/lib/sidekiq/middleware/i18n.rb +2 -0
- data/lib/sidekiq/monitor.rb +4 -8
- data/lib/sidekiq/paginator.rb +18 -2
- data/lib/sidekiq/processor.rb +26 -18
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +46 -67
- data/lib/sidekiq/redis_client_adapter.rb +6 -3
- data/lib/sidekiq/redis_connection.rb +16 -5
- data/lib/sidekiq/ring_buffer.rb +1 -0
- data/lib/sidekiq/scheduled.rb +6 -7
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -333
- data/lib/sidekiq/transaction_aware_client.rb +13 -5
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
- data/lib/sidekiq/tui/tabs/busy.rb +104 -0
- data/lib/sidekiq/tui/tabs/dead.rb +14 -0
- data/lib/sidekiq/tui/tabs/home.rb +115 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
- data/lib/sidekiq/tui/tabs/queues.rb +83 -0
- data/lib/sidekiq/tui/tabs/retries.rb +14 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +390 -0
- data/lib/sidekiq/version.rb +2 -2
- data/lib/sidekiq/web/action.rb +146 -83
- data/lib/sidekiq/web/application.rb +372 -328
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +102 -44
- data/lib/sidekiq/web/router.rb +61 -77
- data/lib/sidekiq/web.rb +65 -151
- data/lib/sidekiq.rb +13 -1
- data/sidekiq.gemspec +9 -8
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +43 -27
- data/web/assets/javascripts/base-charts.js +30 -16
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/dashboard.js +1 -1
- data/web/assets/javascripts/metrics.js +16 -34
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +3 -1
- data/web/locales/cs.yml +2 -0
- data/web/locales/da.yml +2 -0
- data/web/locales/de.yml +2 -0
- data/web/locales/el.yml +2 -0
- data/web/locales/en.yml +8 -0
- data/web/locales/es.yml +25 -2
- data/web/locales/fa.yml +3 -1
- data/web/locales/fr.yml +3 -1
- data/web/locales/gd.yml +13 -1
- data/web/locales/he.yml +3 -1
- data/web/locales/hi.yml +36 -0
- data/web/locales/it.yml +9 -0
- data/web/locales/ja.yml +2 -0
- data/web/locales/ko.yml +2 -0
- data/web/locales/lt.yml +2 -0
- data/web/locales/nb.yml +2 -0
- data/web/locales/nl.yml +2 -0
- data/web/locales/pl.yml +2 -0
- data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
- data/web/locales/pt.yml +2 -0
- data/web/locales/ru.yml +2 -0
- data/web/locales/sv.yml +2 -0
- data/web/locales/ta.yml +83 -47
- data/web/locales/tr.yml +2 -0
- data/web/locales/uk.yml +7 -5
- data/web/locales/ur.yml +3 -1
- data/web/locales/vi.yml +2 -0
- data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
- data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
- data/web/views/_footer.html.erb +32 -0
- data/web/views/_job_info.html.erb +115 -0
- data/web/views/_metrics_period_select.html.erb +15 -0
- data/web/views/_nav.html.erb +45 -0
- data/web/views/_paging.html.erb +26 -0
- data/web/views/_poll_link.html.erb +4 -0
- data/web/views/{_summary.erb → _summary.html.erb} +16 -16
- data/web/views/busy.html.erb +155 -0
- data/web/views/dashboard.html.erb +104 -0
- data/web/views/dead.html.erb +38 -0
- data/web/views/filtering.html.erb +6 -0
- data/web/views/layout.html.erb +26 -0
- data/web/views/metrics.html.erb +85 -0
- data/web/views/metrics_for_job.html.erb +58 -0
- data/web/views/morgue.html.erb +69 -0
- data/web/views/profiles.html.erb +43 -0
- data/web/views/queue.html.erb +57 -0
- data/web/views/queues.html.erb +46 -0
- data/web/views/retries.html.erb +77 -0
- data/web/views/retry.html.erb +39 -0
- data/web/views/scheduled.html.erb +64 -0
- data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
- metadata +65 -47
- data/lib/sidekiq/web/csrf_protection.rb +0 -183
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -163
- data/web/assets/stylesheets/application.css +0 -759
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/views/_footer.erb +0 -34
- data/web/views/_job_info.erb +0 -105
- data/web/views/_metrics_period_select.erb +0 -12
- data/web/views/_nav.erb +0 -52
- data/web/views/_paging.erb +0 -25
- data/web/views/_poll_link.erb +0 -4
- data/web/views/_status.erb +0 -4
- data/web/views/busy.erb +0 -148
- data/web/views/dashboard.erb +0 -108
- data/web/views/dead.erb +0 -34
- data/web/views/filtering.erb +0 -6
- data/web/views/layout.erb +0 -42
- data/web/views/metrics.erb +0 -90
- data/web/views/metrics_for_job.erb +0 -59
- data/web/views/morgue.erb +0 -74
- data/web/views/queue.erb +0 -55
- data/web/views/queues.erb +0 -44
- data/web/views/retries.erb +0 -79
- data/web/views/retry.erb +0 -34
- data/web/views/scheduled.erb +0 -56
data/lib/sidekiq/cli.rb
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
$stdout.sync = true
|
|
4
4
|
|
|
5
|
-
require "yaml"
|
|
6
|
-
require "singleton"
|
|
7
5
|
require "optparse"
|
|
8
6
|
require "erb"
|
|
9
7
|
require "fileutils"
|
|
@@ -17,7 +15,6 @@ require "sidekiq/launcher"
|
|
|
17
15
|
module Sidekiq # :nodoc:
|
|
18
16
|
class CLI
|
|
19
17
|
include Sidekiq::Component
|
|
20
|
-
include Singleton unless $TESTING
|
|
21
18
|
|
|
22
19
|
attr_accessor :launcher
|
|
23
20
|
attr_accessor :environment
|
|
@@ -31,6 +28,10 @@ module Sidekiq # :nodoc:
|
|
|
31
28
|
validate!
|
|
32
29
|
end
|
|
33
30
|
|
|
31
|
+
def self.instance
|
|
32
|
+
@instance ||= new
|
|
33
|
+
end
|
|
34
|
+
|
|
34
35
|
def jruby?
|
|
35
36
|
defined?(::JRUBY_VERSION)
|
|
36
37
|
end
|
|
@@ -47,7 +48,7 @@ module Sidekiq # :nodoc:
|
|
|
47
48
|
logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
|
|
48
49
|
|
|
49
50
|
self_read, self_write = IO.pipe
|
|
50
|
-
sigs = %w[INT TERM TTIN TSTP]
|
|
51
|
+
sigs = %w[INT TERM INFO TTIN TSTP]
|
|
51
52
|
# USR1 and USR2 don't work on the JVM
|
|
52
53
|
sigs << "USR2" if Sidekiq.pro? && !jruby?
|
|
53
54
|
sigs.each do |sig|
|
|
@@ -74,7 +75,7 @@ module Sidekiq # :nodoc:
|
|
|
74
75
|
# fire startup and start multithreading.
|
|
75
76
|
info = @config.redis_info
|
|
76
77
|
ver = Gem::Version.new(info["redis_version"])
|
|
77
|
-
raise "You are
|
|
78
|
+
raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
|
|
78
79
|
|
|
79
80
|
maxmemory_policy = info["maxmemory_policy"]
|
|
80
81
|
if maxmemory_policy != "noeviction" && maxmemory_policy != ""
|
|
@@ -208,14 +209,25 @@ module Sidekiq # :nodoc:
|
|
|
208
209
|
cli.logger.warn "<no backtrace available>"
|
|
209
210
|
end
|
|
210
211
|
end
|
|
212
|
+
},
|
|
213
|
+
"INFO" => ->(cli) {
|
|
214
|
+
cli.logger.error { "DEPRECATED: the INFO signal does not work on Linux, use TTIN instead." }
|
|
215
|
+
Thread.list.each do |thread|
|
|
216
|
+
cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
|
|
217
|
+
if thread.backtrace
|
|
218
|
+
cli.logger.warn thread.backtrace.join("\n")
|
|
219
|
+
else
|
|
220
|
+
cli.logger.warn "<no backtrace available>"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
211
223
|
}
|
|
212
224
|
}
|
|
213
|
-
UNHANDLED_SIGNAL_HANDLER = ->(cli) { cli.logger.info "No signal handler registered, ignoring" }
|
|
214
|
-
SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
|
|
215
225
|
|
|
216
226
|
def handle_signal(sig)
|
|
217
227
|
logger.debug "Got #{sig} signal"
|
|
218
|
-
SIGNAL_HANDLERS[sig]
|
|
228
|
+
hndlr = SIGNAL_HANDLERS[sig]
|
|
229
|
+
hndlr ? hndlr.call(self) :
|
|
230
|
+
logger.warn("No #{sig} signal handler registered, ignoring")
|
|
219
231
|
end
|
|
220
232
|
|
|
221
233
|
private
|
|
@@ -298,29 +310,18 @@ module Sidekiq # :nodoc:
|
|
|
298
310
|
|
|
299
311
|
if File.directory?(@config[:require])
|
|
300
312
|
require "rails"
|
|
301
|
-
if ::Rails::VERSION::MAJOR <
|
|
302
|
-
warn "Sidekiq #{Sidekiq::VERSION} only supports Rails
|
|
313
|
+
if ::Rails::VERSION::MAJOR < 7
|
|
314
|
+
warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 7+"
|
|
303
315
|
end
|
|
304
316
|
require "sidekiq/rails"
|
|
305
317
|
require File.expand_path("#{@config[:require]}/config/environment.rb")
|
|
306
|
-
@config[:tag] ||= default_tag
|
|
318
|
+
@config[:tag] ||= default_tag(::Rails.root)
|
|
307
319
|
else
|
|
308
320
|
require @config[:require]
|
|
321
|
+
@config[:tag] ||= default_tag
|
|
309
322
|
end
|
|
310
323
|
end
|
|
311
324
|
|
|
312
|
-
def default_tag
|
|
313
|
-
dir = ::Rails.root
|
|
314
|
-
name = File.basename(dir)
|
|
315
|
-
prevdir = File.dirname(dir) # Capistrano release directory?
|
|
316
|
-
if name.to_i != 0 && prevdir
|
|
317
|
-
if File.basename(prevdir) == "releases"
|
|
318
|
-
return File.basename(File.dirname(prevdir))
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
name
|
|
322
|
-
end
|
|
323
|
-
|
|
324
325
|
def validate!
|
|
325
326
|
if !File.exist?(@config[:require]) ||
|
|
326
327
|
(File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
|
|
@@ -395,12 +396,18 @@ module Sidekiq # :nodoc:
|
|
|
395
396
|
end
|
|
396
397
|
|
|
397
398
|
def initialize_logger
|
|
398
|
-
|
|
399
|
+
if @config[:verbose] || ENV["DEBUG_INVOCATION"] == "1"
|
|
400
|
+
# DEBUG_INVOCATION is a systemd-ism triggered by
|
|
401
|
+
# RestartMode=debug. We turn on debugging when the
|
|
402
|
+
# sidekiq process crashes and is restarted with this flag.
|
|
403
|
+
@config.logger.level = ::Logger::DEBUG
|
|
404
|
+
end
|
|
399
405
|
end
|
|
400
406
|
|
|
401
407
|
def parse_config(path)
|
|
402
408
|
erb = ERB.new(File.read(path), trim_mode: "-")
|
|
403
409
|
erb.filename = File.expand_path(path)
|
|
410
|
+
require "yaml"
|
|
404
411
|
opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
|
|
405
412
|
|
|
406
413
|
if opts.respond_to? :deep_symbolize_keys!
|
data/lib/sidekiq/client.rb
CHANGED
|
@@ -67,9 +67,7 @@ module Sidekiq
|
|
|
67
67
|
c.pipelined do |p|
|
|
68
68
|
p.hsetnx(key, "cancelled", Time.now.to_i)
|
|
69
69
|
p.hget(key, "cancelled")
|
|
70
|
-
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
|
|
71
|
-
# TODO When Redis 7.2 is required
|
|
72
|
-
# p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
|
70
|
+
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
|
73
71
|
end
|
|
74
72
|
end
|
|
75
73
|
result.to_i
|
|
@@ -119,6 +117,14 @@ module Sidekiq
|
|
|
119
117
|
# larger than 1000 but YMMV based on network quality, size of job args, etc.
|
|
120
118
|
# A large number of jobs can cause a bit of Redis command processing latency.
|
|
121
119
|
#
|
|
120
|
+
# Accepts an `:at` option to schedule the jobs for future execution. It
|
|
121
|
+
# accepts either a single Numeric timestamp (or seconds-from-now) applied
|
|
122
|
+
# to every job, or an Array of Numeric values with the same size as `args`
|
|
123
|
+
# to schedule each job at its corresponding time.
|
|
124
|
+
#
|
|
125
|
+
# Accepts an additional `:spread_interval` option (in seconds) to randomly spread
|
|
126
|
+
# the jobs schedule times over the specified interval.
|
|
127
|
+
#
|
|
122
128
|
# Takes the same arguments as #push except that args is expected to be
|
|
123
129
|
# an Array of Arrays. All other keys are duplicated for each job. Each job
|
|
124
130
|
# is run through the client middleware pipeline and each job gets its own Job ID
|
|
@@ -131,15 +137,28 @@ module Sidekiq
|
|
|
131
137
|
# push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
|
|
132
138
|
#
|
|
133
139
|
def push_bulk(items)
|
|
134
|
-
batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
|
|
135
140
|
args = items["args"]
|
|
136
|
-
at = items.delete("at")
|
|
141
|
+
at = items.delete("at") || items.delete(:at)
|
|
137
142
|
raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
|
|
138
143
|
raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
|
|
139
144
|
|
|
140
145
|
jid = items.delete("jid")
|
|
141
146
|
raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
|
|
142
147
|
|
|
148
|
+
spread_interval = items.delete(:spread_interval) || items.delete("spread_interval")
|
|
149
|
+
raise ArgumentError, "Jobs 'spread_interval' must be a positive Numeric" if spread_interval && (!spread_interval.is_a?(Numeric) || spread_interval <= 0)
|
|
150
|
+
raise ArgumentError, "Only one of 'at' or 'spread_interval' can be provided" if at && spread_interval
|
|
151
|
+
|
|
152
|
+
if !at && spread_interval
|
|
153
|
+
# Do not use spread interval smaller than pooling interval.
|
|
154
|
+
spread_interval = [spread_interval, 5].max
|
|
155
|
+
now = Time.now.to_f
|
|
156
|
+
at = args.map { now + rand * spread_interval }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Use a smaller batch size by default for scheduled jobs since adding to sorted sets is more costly.
|
|
160
|
+
batch_size = items.delete(:batch_size) || items.delete("batch_size") || (at ? 100 : 1_000)
|
|
161
|
+
|
|
143
162
|
normed = normalize_item(items)
|
|
144
163
|
slice_index = 0
|
|
145
164
|
result = args.each_slice(batch_size).flat_map do |slice|
|
|
@@ -266,22 +285,21 @@ module Sidekiq
|
|
|
266
285
|
if payloads.first.key?("at")
|
|
267
286
|
conn.zadd("schedule", payloads.flat_map { |hash|
|
|
268
287
|
at = hash["at"].to_s
|
|
269
|
-
# ActiveJob sets
|
|
270
|
-
hash.
|
|
271
|
-
# TODO: Use hash.except("at") when support for Ruby 2.7 is dropped
|
|
272
|
-
hash = hash.dup
|
|
273
|
-
hash.delete("at")
|
|
288
|
+
# ActiveJob sets enqueued_at but the job has not been enqueued yet
|
|
289
|
+
hash = hash.except("enqueued_at", "at")
|
|
274
290
|
[at, Sidekiq.dump_json(hash)]
|
|
275
291
|
})
|
|
276
292
|
else
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
293
|
+
now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) # milliseconds since the epoch
|
|
294
|
+
grouped_queues = payloads.group_by { |job| job["queue"] }
|
|
295
|
+
conn.sadd("queues", grouped_queues.keys)
|
|
296
|
+
grouped_queues.each do |queue, grouped_payloads|
|
|
297
|
+
to_push = grouped_payloads.map { |entry|
|
|
298
|
+
entry["enqueued_at"] = now
|
|
299
|
+
Sidekiq.dump_json(entry)
|
|
300
|
+
}
|
|
301
|
+
conn.lpush("queue:#{queue}", to_push)
|
|
302
|
+
end
|
|
285
303
|
end
|
|
286
304
|
end
|
|
287
305
|
end
|
data/lib/sidekiq/component.rb
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Sidekiq
|
|
4
|
+
# Ruby's default thread priority is 0, which uses 100ms time slices.
|
|
5
|
+
# This can lead to some surprising thread starvation; if using a lot of
|
|
6
|
+
# CPU-heavy concurrency, it may take several seconds before a Thread gets
|
|
7
|
+
# on the CPU.
|
|
8
|
+
#
|
|
9
|
+
# Negative priorities lower the timeslice by half, so -1 = 50ms, -2 = 25ms, etc.
|
|
10
|
+
# With more frequent timeslices, we reduce the risk of unintentional timeouts
|
|
11
|
+
# and starvation.
|
|
12
|
+
#
|
|
13
|
+
# Customize like so:
|
|
14
|
+
#
|
|
15
|
+
# Sidekiq.configure_server do |cfg|
|
|
16
|
+
# cfg.thread_priority = 0
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
DEFAULT_THREAD_PRIORITY = -1
|
|
20
|
+
|
|
4
21
|
##
|
|
5
|
-
# Sidekiq::Component
|
|
22
|
+
# Sidekiq::Component provides a set of utility methods depending only
|
|
23
|
+
# on Sidekiq::Config. It assumes a config instance is available at @config.
|
|
6
24
|
module Component # :nodoc:
|
|
7
25
|
attr_reader :config
|
|
8
26
|
|
|
27
|
+
# This is epoch milliseconds, appropriate for persistence
|
|
28
|
+
def real_ms
|
|
29
|
+
::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# used for time difference and relative comparisons, not persistence.
|
|
33
|
+
def mono_ms
|
|
34
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
|
|
35
|
+
end
|
|
36
|
+
|
|
9
37
|
def watchdog(last_words)
|
|
10
38
|
yield
|
|
11
39
|
rescue Exception => ex
|
|
@@ -13,11 +41,11 @@ module Sidekiq
|
|
|
13
41
|
raise ex
|
|
14
42
|
end
|
|
15
43
|
|
|
16
|
-
def safe_thread(name, &block)
|
|
44
|
+
def safe_thread(name, priority: nil, &block)
|
|
17
45
|
Thread.new do
|
|
18
46
|
Thread.current.name = "sidekiq.#{name}"
|
|
19
47
|
watchdog(name, &block)
|
|
20
|
-
end
|
|
48
|
+
end.tap { |t| t.priority = (priority || config.thread_priority || DEFAULT_THREAD_PRIORITY) }
|
|
21
49
|
end
|
|
22
50
|
|
|
23
51
|
def logger
|
|
@@ -29,6 +57,9 @@ module Sidekiq
|
|
|
29
57
|
end
|
|
30
58
|
|
|
31
59
|
def tid
|
|
60
|
+
# We XOR with PID to ensure Thread IDs changes after fork.
|
|
61
|
+
# I'm unclear why we don't multiply the two values to better guarantee
|
|
62
|
+
# a unique value but it's been this way for quite a while now. #3685
|
|
32
63
|
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
|
33
64
|
end
|
|
34
65
|
|
|
@@ -86,5 +117,16 @@ module Sidekiq
|
|
|
86
117
|
end.join(", ")
|
|
87
118
|
}>"
|
|
88
119
|
end
|
|
120
|
+
|
|
121
|
+
def default_tag(dir = Dir.pwd)
|
|
122
|
+
name = File.basename(dir)
|
|
123
|
+
prevdir = File.dirname(dir) # Capistrano release directory?
|
|
124
|
+
if name.to_i != 0 && prevdir
|
|
125
|
+
if File.basename(prevdir) == "releases"
|
|
126
|
+
return File.basename(File.dirname(prevdir))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
name
|
|
130
|
+
end
|
|
89
131
|
end
|
|
90
132
|
end
|
data/lib/sidekiq/config.rb
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
|
-
|
|
5
|
-
require "set"
|
|
6
4
|
require "sidekiq/redis_connection"
|
|
7
5
|
|
|
8
6
|
module Sidekiq
|
|
@@ -19,16 +17,16 @@ module Sidekiq
|
|
|
19
17
|
poll_interval_average: nil,
|
|
20
18
|
average_scheduled_poll_interval: 5,
|
|
21
19
|
on_complex_arguments: :raise,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
20
|
+
# if the Iterable job runs longer than this value (in seconds), then the job
|
|
21
|
+
# will be interrupted after the current iteration and re-enqueued at the back of the queue
|
|
22
|
+
max_iteration_runtime: nil,
|
|
26
23
|
error_handlers: [],
|
|
27
24
|
death_handlers: [],
|
|
28
25
|
lifecycle_events: {
|
|
29
26
|
startup: [],
|
|
30
27
|
quiet: [],
|
|
31
28
|
shutdown: [],
|
|
29
|
+
exit: [],
|
|
32
30
|
# triggers when we fire the first heartbeat on startup OR repairing a network partition
|
|
33
31
|
heartbeat: [],
|
|
34
32
|
# triggers on EVERY heartbeat call, every 10 seconds
|
|
@@ -37,16 +35,28 @@ module Sidekiq
|
|
|
37
35
|
dead_max_jobs: 10_000,
|
|
38
36
|
dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
|
|
39
37
|
reloader: proc { |&block| block.call },
|
|
40
|
-
backtrace_cleaner: ->(backtrace) { backtrace }
|
|
38
|
+
backtrace_cleaner: ->(backtrace) { backtrace },
|
|
39
|
+
logged_job_attributes: ["bid", "tags"],
|
|
40
|
+
redis_idle_timeout: nil
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
Sidekiq::Context.with(ctx) do
|
|
45
|
+
dev = cfg[:environment] == "development"
|
|
46
|
+
fancy = dev && $stdout.tty? # 🎩
|
|
47
|
+
# Weird logic here but we want to show the backtrace in local
|
|
48
|
+
# development or if verbose logging is enabled.
|
|
49
|
+
#
|
|
50
|
+
# `full_message` contains the error class, message and backtrace
|
|
51
|
+
# `detailed_message` contains the error class and message
|
|
52
|
+
#
|
|
53
|
+
# Absolutely terrible API names. Not useful at all to have two
|
|
54
|
+
# methods with similar but obscure names.
|
|
55
|
+
if dev || cfg.logger.debug?
|
|
56
|
+
cfg.logger.info { ex.full_message(highlight: fancy) }
|
|
57
|
+
else
|
|
58
|
+
cfg.logger.info { ex.detailed_message(highlight: fancy) }
|
|
59
|
+
end
|
|
50
60
|
end
|
|
51
61
|
}
|
|
52
62
|
|
|
@@ -60,6 +70,7 @@ module Sidekiq
|
|
|
60
70
|
|
|
61
71
|
def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
|
|
62
72
|
attr_reader :capsules
|
|
73
|
+
attr_accessor :thread_priority
|
|
63
74
|
|
|
64
75
|
def inspect
|
|
65
76
|
"#<#{self.class.name} @options=#{
|
|
@@ -135,11 +146,15 @@ module Sidekiq
|
|
|
135
146
|
@redis_config = @redis_config.merge(hash)
|
|
136
147
|
end
|
|
137
148
|
|
|
149
|
+
def reap_idle_redis_connections(timeout = 60)
|
|
150
|
+
self[:redis_idle_timeout] = timeout
|
|
151
|
+
end
|
|
152
|
+
|
|
138
153
|
def redis_pool
|
|
139
154
|
Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
|
|
140
155
|
end
|
|
141
156
|
|
|
142
|
-
|
|
157
|
+
def local_redis_pool
|
|
143
158
|
# this is our internal client/housekeeping pool. each capsule has its
|
|
144
159
|
# own pool for executing threads.
|
|
145
160
|
@redis ||= new_redis_pool(10, "internal")
|
|
@@ -240,7 +255,7 @@ module Sidekiq
|
|
|
240
255
|
# Register a proc to handle any error which occurs within the Sidekiq process.
|
|
241
256
|
#
|
|
242
257
|
# Sidekiq.configure_server do |config|
|
|
243
|
-
# config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
|
|
258
|
+
# config.error_handlers << proc {|ex,ctx_hash,config| MyErrorService.notify(ex, ctx_hash) }
|
|
244
259
|
# end
|
|
245
260
|
#
|
|
246
261
|
# The default error handler logs errors to @logger.
|
|
@@ -249,7 +264,7 @@ module Sidekiq
|
|
|
249
264
|
end
|
|
250
265
|
|
|
251
266
|
# Register a block to run at a point in the Sidekiq lifecycle.
|
|
252
|
-
# :startup, :quiet or :
|
|
267
|
+
# :startup, :quiet, :shutdown, or :exit are valid events.
|
|
253
268
|
#
|
|
254
269
|
# Sidekiq.configure_server do |config|
|
|
255
270
|
# config.on(:shutdown) do
|
|
@@ -282,24 +297,14 @@ module Sidekiq
|
|
|
282
297
|
@logger = logger
|
|
283
298
|
end
|
|
284
299
|
|
|
285
|
-
private def parameter_size(handler)
|
|
286
|
-
target = handler.is_a?(Proc) ? handler : handler.method(:call)
|
|
287
|
-
target.parameters.size
|
|
288
|
-
end
|
|
289
|
-
|
|
290
300
|
# INTERNAL USE ONLY
|
|
291
301
|
def handle_exception(ex, ctx = {})
|
|
292
|
-
if @options[:error_handlers].
|
|
293
|
-
|
|
302
|
+
if @options[:error_handlers].empty?
|
|
303
|
+
logger.error { "No error handlers configured, logging exception directly" }
|
|
304
|
+
logger.error { ex }
|
|
294
305
|
end
|
|
295
306
|
@options[:error_handlers].each do |handler|
|
|
296
|
-
|
|
297
|
-
# TODO Remove in 8.0
|
|
298
|
-
logger.info { "DEPRECATION: Sidekiq exception handlers now take three arguments, see #{handler}" }
|
|
299
|
-
handler.call(ex, {_config: self}.merge(ctx))
|
|
300
|
-
else
|
|
301
|
-
handler.call(ex, ctx, self)
|
|
302
|
-
end
|
|
307
|
+
handler.call(ex, ctx, self)
|
|
303
308
|
rescue Exception => e
|
|
304
309
|
l = logger
|
|
305
310
|
l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
|
data/lib/sidekiq/embedded.rb
CHANGED
|
@@ -34,6 +34,7 @@ module Sidekiq
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
36
|
def housekeeping
|
|
37
|
+
@config[:tag] ||= default_tag
|
|
37
38
|
logger.info "Running in #{RUBY_DESCRIPTION}"
|
|
38
39
|
logger.info Sidekiq::LICENSE
|
|
39
40
|
logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
|
|
@@ -42,7 +43,7 @@ module Sidekiq
|
|
|
42
43
|
# fire startup and start multithreading.
|
|
43
44
|
info = config.redis_info
|
|
44
45
|
ver = Gem::Version.new(info["redis_version"])
|
|
45
|
-
raise "You are
|
|
46
|
+
raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
|
|
46
47
|
|
|
47
48
|
maxmemory_policy = info["maxmemory_policy"]
|
|
48
49
|
if maxmemory_policy != "noeviction"
|
data/lib/sidekiq/fetch.rb
CHANGED
data/lib/sidekiq/iterable_job.rb
CHANGED
data/lib/sidekiq/job/iterable.rb
CHANGED
|
@@ -32,8 +32,14 @@ module Sidekiq
|
|
|
32
32
|
@_runtime = 0
|
|
33
33
|
@_args = nil
|
|
34
34
|
@_cancelled = nil
|
|
35
|
+
@current_object = nil
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
# Access to the current object while iterating.
|
|
39
|
+
# This value is not reset so the latest element is
|
|
40
|
+
# explicitly available to cleanup/complete callbacks.
|
|
41
|
+
attr_reader :current_object
|
|
42
|
+
|
|
37
43
|
def arguments
|
|
38
44
|
@_args
|
|
39
45
|
end
|
|
@@ -49,14 +55,12 @@ module Sidekiq
|
|
|
49
55
|
def cancel!
|
|
50
56
|
return @_cancelled if cancelled?
|
|
51
57
|
|
|
52
|
-
key =
|
|
58
|
+
key = iteration_key
|
|
53
59
|
_, result, _ = Sidekiq.redis do |c|
|
|
54
60
|
c.pipelined do |p|
|
|
55
61
|
p.hsetnx(key, "cancelled", Time.now.to_i)
|
|
56
62
|
p.hget(key, "cancelled")
|
|
57
|
-
|
|
58
|
-
# p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
|
59
|
-
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
|
|
63
|
+
p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
|
|
60
64
|
end
|
|
61
65
|
end
|
|
62
66
|
@_cancelled = result.to_i
|
|
@@ -66,6 +70,10 @@ module Sidekiq
|
|
|
66
70
|
@_cancelled
|
|
67
71
|
end
|
|
68
72
|
|
|
73
|
+
def cursor
|
|
74
|
+
@_cursor.freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
69
77
|
# A hook to override that will be called when the job starts iterating.
|
|
70
78
|
#
|
|
71
79
|
# It is called only once, for the first time.
|
|
@@ -93,6 +101,11 @@ module Sidekiq
|
|
|
93
101
|
def on_stop
|
|
94
102
|
end
|
|
95
103
|
|
|
104
|
+
# A hook to override that will be called when the job is cancelled.
|
|
105
|
+
#
|
|
106
|
+
def on_cancel
|
|
107
|
+
end
|
|
108
|
+
|
|
96
109
|
# A hook to override that will be called when the job finished iterating.
|
|
97
110
|
#
|
|
98
111
|
def on_complete
|
|
@@ -130,7 +143,7 @@ module Sidekiq
|
|
|
130
143
|
fetch_previous_iteration_state
|
|
131
144
|
|
|
132
145
|
@_executions += 1
|
|
133
|
-
@_start_time =
|
|
146
|
+
@_start_time = mono_now
|
|
134
147
|
|
|
135
148
|
enumerator = build_enumerator(*args, cursor: @_cursor)
|
|
136
149
|
unless enumerator
|
|
@@ -164,7 +177,7 @@ module Sidekiq
|
|
|
164
177
|
private
|
|
165
178
|
|
|
166
179
|
def is_cancelled?
|
|
167
|
-
@_cancelled = Sidekiq.redis { |c| c.hget(
|
|
180
|
+
@_cancelled = Sidekiq.redis { |c| c.hget(iteration_key, "cancelled") }
|
|
168
181
|
end
|
|
169
182
|
|
|
170
183
|
def fetch_previous_iteration_state
|
|
@@ -184,34 +197,40 @@ module Sidekiq
|
|
|
184
197
|
|
|
185
198
|
def iterate_with_enumerator(enumerator, arguments)
|
|
186
199
|
if is_cancelled?
|
|
200
|
+
on_cancel
|
|
187
201
|
logger.info { "Job cancelled" }
|
|
188
202
|
return true
|
|
189
203
|
end
|
|
190
204
|
|
|
191
205
|
time_limit = Sidekiq.default_configuration[:timeout]
|
|
192
206
|
found_record = false
|
|
193
|
-
state_flushed_at =
|
|
207
|
+
state_flushed_at = mono_now
|
|
194
208
|
|
|
195
209
|
enumerator.each do |object, cursor|
|
|
196
210
|
found_record = true
|
|
197
211
|
@_cursor = cursor
|
|
212
|
+
@current_object = object
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
if
|
|
214
|
+
interrupt_job = interrupted? || should_interrupt?
|
|
215
|
+
if mono_now - state_flushed_at >= STATE_FLUSH_INTERVAL || interrupt_job
|
|
201
216
|
_, _, cancelled = flush_state
|
|
202
|
-
state_flushed_at =
|
|
217
|
+
state_flushed_at = mono_now
|
|
203
218
|
if cancelled
|
|
204
219
|
@_cancelled = true
|
|
220
|
+
on_cancel
|
|
205
221
|
logger.info { "Job cancelled" }
|
|
206
222
|
return true
|
|
207
223
|
end
|
|
208
224
|
end
|
|
209
225
|
|
|
210
|
-
return false if
|
|
226
|
+
return false if interrupt_job
|
|
211
227
|
|
|
212
|
-
verify_iteration_time(time_limit
|
|
228
|
+
verify_iteration_time(time_limit) do
|
|
213
229
|
around_iteration do
|
|
214
230
|
each_iteration(object, *arguments)
|
|
231
|
+
rescue Exception
|
|
232
|
+
flush_state
|
|
233
|
+
raise
|
|
215
234
|
end
|
|
216
235
|
end
|
|
217
236
|
end
|
|
@@ -219,16 +238,16 @@ module Sidekiq
|
|
|
219
238
|
logger.debug("Enumerator found nothing to iterate!") unless found_record
|
|
220
239
|
true
|
|
221
240
|
ensure
|
|
222
|
-
@_runtime += (
|
|
241
|
+
@_runtime += (mono_now - @_start_time)
|
|
223
242
|
end
|
|
224
243
|
|
|
225
|
-
def verify_iteration_time(time_limit
|
|
226
|
-
start =
|
|
244
|
+
def verify_iteration_time(time_limit)
|
|
245
|
+
start = mono_now
|
|
227
246
|
yield
|
|
228
|
-
finish =
|
|
247
|
+
finish = mono_now
|
|
229
248
|
total = finish - start
|
|
230
249
|
if total > time_limit
|
|
231
|
-
logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d)
|
|
250
|
+
logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d). This can lead to job processing problems during deploys" % [total, time_limit] }
|
|
232
251
|
end
|
|
233
252
|
end
|
|
234
253
|
|
|
@@ -254,6 +273,11 @@ module Sidekiq
|
|
|
254
273
|
end
|
|
255
274
|
end
|
|
256
275
|
|
|
276
|
+
def should_interrupt?
|
|
277
|
+
max_iteration_runtime = Sidekiq.default_configuration[:max_iteration_runtime]
|
|
278
|
+
max_iteration_runtime && (mono_now - @_start_time > max_iteration_runtime)
|
|
279
|
+
end
|
|
280
|
+
|
|
257
281
|
def flush_state
|
|
258
282
|
key = iteration_key
|
|
259
283
|
state = {
|
|
@@ -265,7 +289,7 @@ module Sidekiq
|
|
|
265
289
|
Sidekiq.redis do |conn|
|
|
266
290
|
conn.multi do |pipe|
|
|
267
291
|
pipe.hset(key, state)
|
|
268
|
-
pipe.expire(key, STATE_TTL)
|
|
292
|
+
pipe.expire(key, STATE_TTL, "nx")
|
|
269
293
|
pipe.hget(key, "cancelled")
|
|
270
294
|
end
|
|
271
295
|
end
|
|
@@ -289,6 +313,10 @@ module Sidekiq
|
|
|
289
313
|
raise "Unexpected thrown value: #{completed.inspect}"
|
|
290
314
|
end
|
|
291
315
|
end
|
|
316
|
+
|
|
317
|
+
def mono_now
|
|
318
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
319
|
+
end
|
|
292
320
|
end
|
|
293
321
|
end
|
|
294
322
|
end
|