sidekiq 6.3.1 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sidekiq might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Changes.md +140 -4
- data/LICENSE.txt +9 -0
- data/README.md +19 -13
- data/bin/sidekiq +4 -9
- data/bin/sidekiqload +71 -76
- data/bin/sidekiqmon +1 -1
- data/lib/generators/sidekiq/job_generator.rb +57 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
- data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
- data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
- data/lib/sidekiq/api.rb +267 -186
- data/lib/sidekiq/capsule.rb +110 -0
- data/lib/sidekiq/cli.rb +82 -78
- data/lib/sidekiq/client.rb +73 -80
- data/lib/sidekiq/{util.rb → component.rb} +13 -42
- data/lib/sidekiq/config.rb +271 -0
- data/lib/sidekiq/deploy.rb +62 -0
- data/lib/sidekiq/embedded.rb +61 -0
- data/lib/sidekiq/fetch.rb +22 -21
- data/lib/sidekiq/job.rb +375 -10
- data/lib/sidekiq/job_logger.rb +16 -28
- data/lib/sidekiq/job_retry.rb +79 -56
- data/lib/sidekiq/job_util.rb +71 -0
- data/lib/sidekiq/launcher.rb +76 -82
- data/lib/sidekiq/logger.rb +9 -44
- data/lib/sidekiq/manager.rb +40 -41
- data/lib/sidekiq/metrics/query.rb +153 -0
- data/lib/sidekiq/metrics/shared.rb +95 -0
- data/lib/sidekiq/metrics/tracking.rb +134 -0
- data/lib/sidekiq/middleware/chain.rb +84 -42
- data/lib/sidekiq/middleware/current_attributes.rb +19 -13
- data/lib/sidekiq/middleware/i18n.rb +6 -4
- data/lib/sidekiq/middleware/modules.rb +21 -0
- data/lib/sidekiq/monitor.rb +1 -1
- data/lib/sidekiq/paginator.rb +16 -8
- data/lib/sidekiq/processor.rb +56 -59
- data/lib/sidekiq/rails.rb +10 -9
- data/lib/sidekiq/redis_client_adapter.rb +118 -0
- data/lib/sidekiq/redis_connection.rb +13 -82
- data/lib/sidekiq/ring_buffer.rb +29 -0
- data/lib/sidekiq/scheduled.rb +75 -37
- data/lib/sidekiq/testing/inline.rb +4 -4
- data/lib/sidekiq/testing.rb +41 -68
- data/lib/sidekiq/transaction_aware_client.rb +44 -0
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web/action.rb +3 -3
- data/lib/sidekiq/web/application.rb +27 -8
- data/lib/sidekiq/web/csrf_protection.rb +3 -3
- data/lib/sidekiq/web/helpers.rb +22 -20
- data/lib/sidekiq/web.rb +6 -17
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +85 -202
- data/sidekiq.gemspec +29 -5
- data/web/assets/javascripts/application.js +58 -26
- data/web/assets/javascripts/base-charts.js +106 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +166 -0
- data/web/assets/javascripts/dashboard.js +3 -240
- data/web/assets/javascripts/metrics.js +236 -0
- data/web/assets/stylesheets/application-dark.css +13 -17
- data/web/assets/stylesheets/application-rtl.css +2 -91
- data/web/assets/stylesheets/application.css +67 -300
- data/web/locales/ar.yml +70 -70
- data/web/locales/cs.yml +62 -62
- data/web/locales/da.yml +52 -52
- data/web/locales/de.yml +65 -65
- data/web/locales/el.yml +43 -24
- data/web/locales/en.yml +82 -69
- data/web/locales/es.yml +68 -68
- data/web/locales/fa.yml +65 -65
- data/web/locales/fr.yml +67 -67
- data/web/locales/he.yml +65 -64
- data/web/locales/hi.yml +59 -59
- data/web/locales/it.yml +53 -53
- data/web/locales/ja.yml +71 -68
- data/web/locales/ko.yml +52 -52
- data/web/locales/lt.yml +66 -66
- data/web/locales/nb.yml +61 -61
- data/web/locales/nl.yml +52 -52
- data/web/locales/pl.yml +45 -45
- data/web/locales/pt-br.yml +63 -55
- data/web/locales/pt.yml +51 -51
- data/web/locales/ru.yml +67 -66
- data/web/locales/sv.yml +53 -53
- data/web/locales/ta.yml +60 -60
- data/web/locales/uk.yml +62 -61
- data/web/locales/ur.yml +64 -64
- data/web/locales/vi.yml +67 -67
- data/web/locales/zh-cn.yml +37 -11
- data/web/locales/zh-tw.yml +42 -8
- data/web/views/_footer.erb +5 -2
- data/web/views/_nav.erb +1 -1
- data/web/views/_summary.erb +1 -1
- data/web/views/busy.erb +9 -4
- data/web/views/dashboard.erb +36 -4
- data/web/views/metrics.erb +80 -0
- data/web/views/metrics_for_job.erb +69 -0
- data/web/views/queue.erb +5 -1
- metadata +75 -27
- data/LICENSE +0 -9
- data/lib/generators/sidekiq/worker_generator.rb +0 -57
- data/lib/sidekiq/delay.rb +0 -41
- data/lib/sidekiq/exception_handler.rb +0 -27
- data/lib/sidekiq/extensions/action_mailer.rb +0 -48
- data/lib/sidekiq/extensions/active_record.rb +0 -43
- data/lib/sidekiq/extensions/class_methods.rb +0 -43
- data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
- data/lib/sidekiq/worker.rb +0 -311
data/lib/sidekiq/rails.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "sidekiq/
|
3
|
+
require "sidekiq/job"
|
4
|
+
require "rails"
|
4
5
|
|
5
6
|
module Sidekiq
|
6
7
|
class Rails < ::Rails::Engine
|
@@ -22,7 +23,7 @@ module Sidekiq
|
|
22
23
|
|
23
24
|
# By including the Options module, we allow AJs to directly control sidekiq features
|
24
25
|
# via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
|
25
|
-
# AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be
|
26
|
+
# AJ retries don't show up in the Sidekiq UI Retries tab, don't save any error data, can't be
|
26
27
|
# manually retried, don't automatically die, etc.
|
27
28
|
#
|
28
29
|
# class SomeJob < ActiveJob::Base
|
@@ -33,17 +34,17 @@ module Sidekiq
|
|
33
34
|
# end
|
34
35
|
initializer "sidekiq.active_job_integration" do
|
35
36
|
ActiveSupport.on_load(:active_job) do
|
36
|
-
include ::Sidekiq::
|
37
|
+
include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
41
|
initializer "sidekiq.rails_logger" do
|
41
|
-
Sidekiq.configure_server do |
|
42
|
-
# This is the integration code necessary so that if
|
42
|
+
Sidekiq.configure_server do |config|
|
43
|
+
# This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
|
43
44
|
# it will appear in the Sidekiq console with all of the job context. See #5021 and
|
44
45
|
# https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
|
45
|
-
unless ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
|
46
|
-
::Rails.logger.extend(::ActiveSupport::Logger.broadcast(
|
46
|
+
unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
|
47
|
+
::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
|
47
48
|
end
|
48
49
|
end
|
49
50
|
end
|
@@ -53,8 +54,8 @@ module Sidekiq
|
|
53
54
|
#
|
54
55
|
# None of this matters on the client-side, only within the Sidekiq process itself.
|
55
56
|
config.after_initialize do
|
56
|
-
Sidekiq.configure_server do |
|
57
|
-
|
57
|
+
Sidekiq.configure_server do |config|
|
58
|
+
config[:reloader] = Sidekiq::Rails::Reloader.new
|
58
59
|
end
|
59
60
|
end
|
60
61
|
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis_client"
|
4
|
+
require "redis_client/decorator"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
class RedisClientAdapter
|
8
|
+
BaseError = RedisClient::Error
|
9
|
+
CommandError = RedisClient::CommandError
|
10
|
+
|
11
|
+
module CompatMethods
|
12
|
+
def info
|
13
|
+
@client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
|
14
|
+
end
|
15
|
+
|
16
|
+
def evalsha(sha, keys, argv)
|
17
|
+
@client.call("EVALSHA", sha, keys.size, *keys, *argv)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# this allows us to use methods like `conn.hmset(...)` instead of having to use
|
23
|
+
# redis-client's native `conn.call("hmset", ...)`
|
24
|
+
def method_missing(*args, &block)
|
25
|
+
@client.call(*args, *block)
|
26
|
+
end
|
27
|
+
ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
|
28
|
+
|
29
|
+
def respond_to_missing?(name, include_private = false)
|
30
|
+
super # Appease the linter. We can't tell what is a valid command.
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
CompatClient = RedisClient::Decorator.create(CompatMethods)
|
35
|
+
|
36
|
+
class CompatClient
|
37
|
+
# underscore methods are not official API
|
38
|
+
def _client
|
39
|
+
@client
|
40
|
+
end
|
41
|
+
|
42
|
+
def _config
|
43
|
+
@client.config
|
44
|
+
end
|
45
|
+
|
46
|
+
def message
|
47
|
+
yield nil, @queue.pop
|
48
|
+
end
|
49
|
+
|
50
|
+
# NB: this method does not return
|
51
|
+
def subscribe(chan)
|
52
|
+
@queue = ::Queue.new
|
53
|
+
|
54
|
+
pubsub = @client.pubsub
|
55
|
+
pubsub.call("subscribe", chan)
|
56
|
+
|
57
|
+
loop do
|
58
|
+
evt = pubsub.next_event
|
59
|
+
next if evt.nil?
|
60
|
+
next unless evt[0] == "message" && evt[1] == chan
|
61
|
+
|
62
|
+
(_, _, msg) = evt
|
63
|
+
@queue << msg
|
64
|
+
yield self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def initialize(options)
|
70
|
+
opts = client_opts(options)
|
71
|
+
@config = if opts.key?(:sentinels)
|
72
|
+
RedisClient.sentinel(**opts)
|
73
|
+
else
|
74
|
+
RedisClient.config(**opts)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def new_client
|
79
|
+
CompatClient.new(@config.new_client)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def client_opts(options)
|
85
|
+
opts = options.dup
|
86
|
+
|
87
|
+
if opts[:namespace]
|
88
|
+
raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature isn't supported by redis-client. " \
|
89
|
+
"Either use the redis adapter or remove the namespace."
|
90
|
+
end
|
91
|
+
|
92
|
+
opts.delete(:size)
|
93
|
+
opts.delete(:pool_timeout)
|
94
|
+
|
95
|
+
if opts[:network_timeout]
|
96
|
+
opts[:timeout] = opts[:network_timeout]
|
97
|
+
opts.delete(:network_timeout)
|
98
|
+
end
|
99
|
+
|
100
|
+
if opts[:driver]
|
101
|
+
opts[:driver] = opts[:driver].to_sym
|
102
|
+
end
|
103
|
+
|
104
|
+
opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
|
105
|
+
opts[:role] = opts[:role].to_sym if opts.key?(:role)
|
106
|
+
opts.delete(:url) if opts.key?(:sentinels)
|
107
|
+
|
108
|
+
# Issue #3303, redis-rb will silently retry an operation.
|
109
|
+
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
110
|
+
# is performed twice but I believe this is much, much rarer
|
111
|
+
# than the reconnect silently fixing a problem; we keep it
|
112
|
+
# on by default.
|
113
|
+
opts[:reconnect_attempts] ||= 1
|
114
|
+
|
115
|
+
opts
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -1,97 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "connection_pool"
|
4
|
-
require "redis"
|
5
4
|
require "uri"
|
5
|
+
require "sidekiq/redis_client_adapter"
|
6
6
|
|
7
7
|
module Sidekiq
|
8
|
-
|
8
|
+
module RedisConnection
|
9
9
|
class << self
|
10
10
|
def create(options = {})
|
11
11
|
symbolized_options = options.transform_keys(&:to_sym)
|
12
|
+
symbolized_options[:url] ||= determine_redis_provider
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
size = if symbolized_options[:size]
|
18
|
-
symbolized_options[:size]
|
19
|
-
elsif Sidekiq.server?
|
20
|
-
# Give ourselves plenty of connections. pool is lazy
|
21
|
-
# so we won't create them until we need them.
|
22
|
-
Sidekiq.options[:concurrency] + 5
|
23
|
-
elsif ENV["RAILS_MAX_THREADS"]
|
24
|
-
Integer(ENV["RAILS_MAX_THREADS"])
|
25
|
-
else
|
26
|
-
5
|
27
|
-
end
|
14
|
+
logger = symbolized_options.delete(:logger)
|
15
|
+
logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
|
28
16
|
|
29
|
-
|
17
|
+
size = symbolized_options.delete(:size) || 5
|
18
|
+
pool_timeout = symbolized_options.delete(:pool_timeout) || 1
|
19
|
+
pool_name = symbolized_options.delete(:pool_name)
|
30
20
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
ConnectionPool.new(timeout: pool_timeout, size: size) do
|
35
|
-
build_client(symbolized_options)
|
21
|
+
redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
|
22
|
+
ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
|
23
|
+
redis_config.new_client
|
36
24
|
end
|
37
25
|
end
|
38
26
|
|
39
27
|
private
|
40
28
|
|
41
|
-
|
42
|
-
#
|
43
|
-
# We need a connection for each Processor.
|
44
|
-
# We need a connection for Pro's real-time change listener
|
45
|
-
# We need a connection to various features to call Redis every few seconds:
|
46
|
-
# - the process heartbeat.
|
47
|
-
# - enterprise's leader election
|
48
|
-
# - enterprise's cron support
|
49
|
-
def verify_sizing(size, concurrency)
|
50
|
-
raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size < (concurrency + 2)
|
51
|
-
end
|
52
|
-
|
53
|
-
def build_client(options)
|
54
|
-
namespace = options[:namespace]
|
55
|
-
|
56
|
-
client = Redis.new client_opts(options)
|
57
|
-
if namespace
|
58
|
-
begin
|
59
|
-
require "redis/namespace"
|
60
|
-
Redis::Namespace.new(namespace, redis: client)
|
61
|
-
rescue LoadError
|
62
|
-
Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
|
63
|
-
"Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
|
64
|
-
exit(-127)
|
65
|
-
end
|
66
|
-
else
|
67
|
-
client
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def client_opts(options)
|
72
|
-
opts = options.dup
|
73
|
-
if opts[:namespace]
|
74
|
-
opts.delete(:namespace)
|
75
|
-
end
|
76
|
-
|
77
|
-
if opts[:network_timeout]
|
78
|
-
opts[:timeout] = opts[:network_timeout]
|
79
|
-
opts.delete(:network_timeout)
|
80
|
-
end
|
81
|
-
|
82
|
-
opts[:driver] ||= Redis::Connection.drivers.last || "ruby"
|
83
|
-
|
84
|
-
# Issue #3303, redis-rb will silently retry an operation.
|
85
|
-
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
86
|
-
# is performed twice but I believe this is much, much rarer
|
87
|
-
# than the reconnect silently fixing a problem; we keep it
|
88
|
-
# on by default.
|
89
|
-
opts[:reconnect_attempts] ||= 1
|
90
|
-
|
91
|
-
opts
|
92
|
-
end
|
93
|
-
|
94
|
-
def log_info(options)
|
29
|
+
def scrub(options)
|
95
30
|
redacted = "REDACTED"
|
96
31
|
|
97
32
|
# Deep clone so we can muck with these options all we want and exclude
|
@@ -109,11 +44,7 @@ module Sidekiq
|
|
109
44
|
scrubbed_options[:sentinels]&.each do |sentinel|
|
110
45
|
sentinel[:password] = redacted if sentinel[:password]
|
111
46
|
end
|
112
|
-
|
113
|
-
Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
|
114
|
-
else
|
115
|
-
Sidekiq.logger.debug("#{Sidekiq::NAME} client with redis options #{scrubbed_options}")
|
116
|
-
end
|
47
|
+
scrubbed_options
|
117
48
|
end
|
118
49
|
|
119
50
|
def determine_redis_provider
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
class RingBuffer
|
5
|
+
include Enumerable
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@buf, :[], :each, :size
|
8
|
+
|
9
|
+
def initialize(size, default = 0)
|
10
|
+
@size = size
|
11
|
+
@buf = Array.new(size, default)
|
12
|
+
@index = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(element)
|
16
|
+
@buf[@index % @size] = element
|
17
|
+
@index += 1
|
18
|
+
element
|
19
|
+
end
|
20
|
+
|
21
|
+
def buffer
|
22
|
+
@buf
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset(default = 0)
|
26
|
+
@buf.fill(default)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/sidekiq/scheduled.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "sidekiq"
|
4
|
-
require "sidekiq/
|
5
|
-
require "sidekiq/api"
|
4
|
+
require "sidekiq/component"
|
6
5
|
|
7
6
|
module Sidekiq
|
8
7
|
module Scheduled
|
9
8
|
SETS = %w[retry schedule]
|
10
9
|
|
11
10
|
class Enq
|
11
|
+
include Sidekiq::Component
|
12
|
+
|
12
13
|
LUA_ZPOPBYSCORE = <<~LUA
|
13
14
|
local key, now = KEYS[1], ARGV[1]
|
14
15
|
local jobs = redis.call("zrangebyscore", key, "-inf", now, "limit", 0, 1)
|
@@ -18,34 +19,43 @@ module Sidekiq
|
|
18
19
|
end
|
19
20
|
LUA
|
20
21
|
|
21
|
-
def initialize
|
22
|
+
def initialize(container)
|
23
|
+
@config = container
|
24
|
+
@client = Sidekiq::Client.new(config: container)
|
25
|
+
@done = false
|
22
26
|
@lua_zpopbyscore_sha = nil
|
23
27
|
end
|
24
28
|
|
25
|
-
def enqueue_jobs(
|
29
|
+
def enqueue_jobs(sorted_sets = SETS)
|
26
30
|
# A job's "score" in Redis is the time at which it should be processed.
|
27
31
|
# Just check Redis for the set of jobs with a timestamp before now.
|
28
|
-
|
32
|
+
redis do |conn|
|
29
33
|
sorted_sets.each do |sorted_set|
|
30
34
|
# Get next item in the queue with score (time to execute) <= now.
|
31
35
|
# We need to go through the list one at a time to reduce the risk of something
|
32
36
|
# going wrong between the time jobs are popped from the scheduled queue and when
|
33
37
|
# they are pushed onto a work queue and losing the jobs.
|
34
|
-
while (job = zpopbyscore(conn, keys: [sorted_set], argv: [now]))
|
35
|
-
|
36
|
-
|
38
|
+
while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s]))
|
39
|
+
@client.push(Sidekiq.load_json(job))
|
40
|
+
logger.debug { "enqueued #{sorted_set}: #{job}" }
|
37
41
|
end
|
38
42
|
end
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
46
|
+
def terminate
|
47
|
+
@done = true
|
48
|
+
end
|
49
|
+
|
42
50
|
private
|
43
51
|
|
44
52
|
def zpopbyscore(conn, keys: nil, argv: nil)
|
45
|
-
|
53
|
+
if @lua_zpopbyscore_sha.nil?
|
54
|
+
@lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE)
|
55
|
+
end
|
46
56
|
|
47
|
-
conn.evalsha(@lua_zpopbyscore_sha, keys
|
48
|
-
rescue
|
57
|
+
conn.evalsha(@lua_zpopbyscore_sha, keys, argv)
|
58
|
+
rescue RedisClient::CommandError => e
|
49
59
|
raise unless e.message.start_with?("NOSCRIPT")
|
50
60
|
|
51
61
|
@lua_zpopbyscore_sha = nil
|
@@ -59,12 +69,13 @@ module Sidekiq
|
|
59
69
|
# just pops the job back onto its original queue so the
|
60
70
|
# workers can pick it up like any other job.
|
61
71
|
class Poller
|
62
|
-
include
|
72
|
+
include Sidekiq::Component
|
63
73
|
|
64
74
|
INITIAL_WAIT = 10
|
65
75
|
|
66
|
-
def initialize
|
67
|
-
@
|
76
|
+
def initialize(config)
|
77
|
+
@config = config
|
78
|
+
@enq = (config[:scheduled_enq] || Sidekiq::Scheduled::Enq).new(config)
|
68
79
|
@sleeper = ConnectionPool::TimedStack.new
|
69
80
|
@done = false
|
70
81
|
@thread = nil
|
@@ -74,12 +85,10 @@ module Sidekiq
|
|
74
85
|
# Shut down this instance, will pause until the thread is dead.
|
75
86
|
def terminate
|
76
87
|
@done = true
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
t.value
|
82
|
-
end
|
88
|
+
@enq.terminate
|
89
|
+
|
90
|
+
@sleeper << 0
|
91
|
+
@thread&.value
|
83
92
|
end
|
84
93
|
|
85
94
|
def start
|
@@ -90,7 +99,7 @@ module Sidekiq
|
|
90
99
|
enqueue
|
91
100
|
wait
|
92
101
|
end
|
93
|
-
|
102
|
+
logger.info("Scheduler exiting...")
|
94
103
|
}
|
95
104
|
end
|
96
105
|
|
@@ -137,13 +146,16 @@ module Sidekiq
|
|
137
146
|
# As we run more processes, the scheduling interval average will approach an even spread
|
138
147
|
# between 0 and poll interval so we don't need this artifical boost.
|
139
148
|
#
|
140
|
-
|
149
|
+
count = process_count
|
150
|
+
interval = poll_interval_average(count)
|
151
|
+
|
152
|
+
if count < 10
|
141
153
|
# For small clusters, calculate a random interval that is ±50% the desired average.
|
142
|
-
|
154
|
+
interval * rand + interval.to_f / 2
|
143
155
|
else
|
144
156
|
# With 10+ processes, we should have enough randomness to get decent polling
|
145
157
|
# across the entire timespan
|
146
|
-
|
158
|
+
interval * rand
|
147
159
|
end
|
148
160
|
end
|
149
161
|
|
@@ -160,38 +172,64 @@ module Sidekiq
|
|
160
172
|
# the same time: the thundering herd problem.
|
161
173
|
#
|
162
174
|
# We only do this if poll_interval_average is unset (the default).
|
163
|
-
def poll_interval_average
|
164
|
-
|
175
|
+
def poll_interval_average(count)
|
176
|
+
@config[:poll_interval_average] || scaled_poll_interval(count)
|
165
177
|
end
|
166
178
|
|
167
179
|
# Calculates an average poll interval based on the number of known Sidekiq processes.
|
168
180
|
# This minimizes a single point of failure by dispersing check-ins but without taxing
|
169
181
|
# Redis if you run many Sidekiq processes.
|
170
|
-
def scaled_poll_interval
|
171
|
-
process_count *
|
182
|
+
def scaled_poll_interval(process_count)
|
183
|
+
process_count * @config[:average_scheduled_poll_interval]
|
172
184
|
end
|
173
185
|
|
174
186
|
def process_count
|
175
|
-
|
176
|
-
# expensive at scale. Cut it down by 90% with this counter.
|
177
|
-
# NB: This method is only called by the scheduler thread so we
|
178
|
-
# don't need to worry about the thread safety of +=.
|
179
|
-
pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
|
187
|
+
pcount = Sidekiq.redis { |conn| conn.scard("processes") }
|
180
188
|
pcount = 1 if pcount == 0
|
181
|
-
@count_calls += 1
|
182
189
|
pcount
|
183
190
|
end
|
184
191
|
|
192
|
+
# A copy of Sidekiq::ProcessSet#cleanup because server
|
193
|
+
# should never depend on sidekiq/api.
|
194
|
+
def cleanup
|
195
|
+
# dont run cleanup more than once per minute
|
196
|
+
return 0 unless redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
|
197
|
+
|
198
|
+
count = 0
|
199
|
+
redis do |conn|
|
200
|
+
procs = conn.sscan("processes").to_a
|
201
|
+
heartbeats = conn.pipelined { |pipeline|
|
202
|
+
procs.each do |key|
|
203
|
+
pipeline.hget(key, "info")
|
204
|
+
end
|
205
|
+
}
|
206
|
+
|
207
|
+
# the hash named key has an expiry of 60 seconds.
|
208
|
+
# if it's not found, that means the process has not reported
|
209
|
+
# in to Redis and probably died.
|
210
|
+
to_prune = procs.select.with_index { |proc, i|
|
211
|
+
heartbeats[i].nil?
|
212
|
+
}
|
213
|
+
count = conn.srem("processes", to_prune) unless to_prune.empty?
|
214
|
+
end
|
215
|
+
count
|
216
|
+
end
|
217
|
+
|
185
218
|
def initial_wait
|
186
|
-
# Have all processes sleep between 5-15 seconds.
|
187
|
-
#
|
219
|
+
# Have all processes sleep between 5-15 seconds. 10 seconds to give time for
|
220
|
+
# the heartbeat to register (if the poll interval is going to be calculated by the number
|
188
221
|
# of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
|
189
222
|
total = 0
|
190
|
-
total += INITIAL_WAIT unless
|
223
|
+
total += INITIAL_WAIT unless @config[:poll_interval_average]
|
191
224
|
total += (5 * rand)
|
192
225
|
|
193
226
|
@sleeper.pop(total)
|
194
227
|
rescue Timeout::Error
|
228
|
+
ensure
|
229
|
+
# periodically clean out the `processes` set in Redis which can collect
|
230
|
+
# references to dead processes over time. The process count affects how
|
231
|
+
# often we scan for scheduled jobs.
|
232
|
+
cleanup
|
195
233
|
end
|
196
234
|
end
|
197
235
|
end
|
@@ -4,7 +4,7 @@ require "sidekiq/testing"
|
|
4
4
|
|
5
5
|
##
|
6
6
|
# The Sidekiq inline infrastructure overrides perform_async so that it
|
7
|
-
# actually calls perform instead. This allows
|
7
|
+
# actually calls perform instead. This allows jobs to be run inline in a
|
8
8
|
# testing environment.
|
9
9
|
#
|
10
10
|
# This is similar to `Resque.inline = true` functionality.
|
@@ -15,8 +15,8 @@ require "sidekiq/testing"
|
|
15
15
|
#
|
16
16
|
# $external_variable = 0
|
17
17
|
#
|
18
|
-
# class
|
19
|
-
# include Sidekiq::
|
18
|
+
# class ExternalJob
|
19
|
+
# include Sidekiq::Job
|
20
20
|
#
|
21
21
|
# def perform
|
22
22
|
# $external_variable = 1
|
@@ -24,7 +24,7 @@ require "sidekiq/testing"
|
|
24
24
|
# end
|
25
25
|
#
|
26
26
|
# assert_equal 0, $external_variable
|
27
|
-
#
|
27
|
+
# ExternalJob.perform_async
|
28
28
|
# assert_equal 1, $external_variable
|
29
29
|
#
|
30
30
|
Sidekiq::Testing.inline!
|