sidekiq 2.15.1 → 4.2.10
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 +7 -0
- data/.github/contributing.md +32 -0
- data/.github/issue_template.md +9 -0
- data/.gitignore +1 -0
- data/.travis.yml +16 -17
- data/3.0-Upgrade.md +70 -0
- data/4.0-Upgrade.md +53 -0
- data/COMM-LICENSE +56 -44
- data/Changes.md +644 -1
- data/Ent-Changes.md +173 -0
- data/Gemfile +27 -0
- data/LICENSE +1 -1
- data/Pro-2.0-Upgrade.md +138 -0
- data/Pro-3.0-Upgrade.md +44 -0
- data/Pro-Changes.md +457 -3
- data/README.md +46 -29
- data/Rakefile +6 -3
- data/bin/sidekiq +4 -0
- data/bin/sidekiqctl +41 -20
- data/bin/sidekiqload +154 -0
- data/code_of_conduct.md +50 -0
- data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
- data/lib/generators/sidekiq/worker_generator.rb +49 -0
- data/lib/sidekiq.rb +141 -29
- data/lib/sidekiq/api.rb +540 -106
- data/lib/sidekiq/cli.rb +131 -71
- data/lib/sidekiq/client.rb +168 -96
- data/lib/sidekiq/core_ext.rb +36 -8
- data/lib/sidekiq/exception_handler.rb +20 -28
- data/lib/sidekiq/extensions/action_mailer.rb +25 -5
- data/lib/sidekiq/extensions/active_record.rb +8 -4
- data/lib/sidekiq/extensions/class_methods.rb +9 -5
- data/lib/sidekiq/extensions/generic_proxy.rb +1 -0
- data/lib/sidekiq/fetch.rb +45 -101
- data/lib/sidekiq/launcher.rb +144 -30
- data/lib/sidekiq/logging.rb +69 -12
- data/lib/sidekiq/manager.rb +90 -140
- data/lib/sidekiq/middleware/chain.rb +18 -5
- data/lib/sidekiq/middleware/i18n.rb +9 -2
- data/lib/sidekiq/middleware/server/active_record.rb +1 -1
- data/lib/sidekiq/middleware/server/logging.rb +11 -11
- data/lib/sidekiq/middleware/server/retry_jobs.rb +98 -44
- data/lib/sidekiq/paginator.rb +20 -8
- data/lib/sidekiq/processor.rb +157 -96
- data/lib/sidekiq/rails.rb +109 -5
- data/lib/sidekiq/redis_connection.rb +70 -24
- data/lib/sidekiq/scheduled.rb +122 -50
- data/lib/sidekiq/testing.rb +171 -31
- data/lib/sidekiq/testing/inline.rb +1 -0
- data/lib/sidekiq/util.rb +31 -5
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +136 -263
- data/lib/sidekiq/web/action.rb +93 -0
- data/lib/sidekiq/web/application.rb +336 -0
- data/lib/sidekiq/web/helpers.rb +278 -0
- data/lib/sidekiq/web/router.rb +100 -0
- data/lib/sidekiq/worker.rb +40 -7
- data/sidekiq.gemspec +18 -14
- data/web/assets/images/favicon.ico +0 -0
- data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
- data/web/assets/javascripts/application.js +67 -19
- data/web/assets/javascripts/dashboard.js +138 -29
- data/web/assets/stylesheets/application.css +267 -406
- data/web/assets/stylesheets/bootstrap.css +4 -8
- data/web/locales/cs.yml +78 -0
- data/web/locales/da.yml +9 -1
- data/web/locales/de.yml +18 -9
- data/web/locales/el.yml +68 -0
- data/web/locales/en.yml +19 -4
- data/web/locales/es.yml +10 -1
- data/web/locales/fa.yml +79 -0
- data/web/locales/fr.yml +50 -32
- data/web/locales/hi.yml +75 -0
- data/web/locales/it.yml +27 -18
- data/web/locales/ja.yml +27 -12
- data/web/locales/ko.yml +8 -3
- data/web/locales/{no.yml → nb.yml} +19 -5
- data/web/locales/nl.yml +8 -3
- data/web/locales/pl.yml +0 -1
- data/web/locales/pt-br.yml +11 -4
- data/web/locales/pt.yml +8 -1
- data/web/locales/ru.yml +39 -21
- data/web/locales/sv.yml +68 -0
- data/web/locales/ta.yml +75 -0
- data/web/locales/uk.yml +76 -0
- data/web/locales/zh-cn.yml +68 -0
- data/web/locales/zh-tw.yml +68 -0
- data/web/views/_footer.erb +17 -0
- data/web/views/_job_info.erb +72 -60
- data/web/views/_nav.erb +58 -25
- data/web/views/_paging.erb +5 -5
- data/web/views/_poll_link.erb +7 -0
- data/web/views/_summary.erb +20 -14
- data/web/views/busy.erb +94 -0
- data/web/views/dashboard.erb +34 -21
- data/web/views/dead.erb +34 -0
- data/web/views/layout.erb +8 -30
- data/web/views/morgue.erb +75 -0
- data/web/views/queue.erb +37 -30
- data/web/views/queues.erb +26 -20
- data/web/views/retries.erb +60 -47
- data/web/views/retry.erb +23 -19
- data/web/views/scheduled.erb +39 -35
- data/web/views/scheduled_job_info.erb +2 -1
- metadata +152 -195
- data/Contributing.md +0 -29
- data/config.ru +0 -18
- data/lib/sidekiq/actor.rb +0 -7
- data/lib/sidekiq/capistrano.rb +0 -54
- data/lib/sidekiq/yaml_patch.rb +0 -21
- data/test/config.yml +0 -11
- data/test/env_based_config.yml +0 -11
- data/test/fake_env.rb +0 -0
- data/test/helper.rb +0 -42
- data/test/test_api.rb +0 -341
- data/test/test_cli.rb +0 -326
- data/test/test_client.rb +0 -211
- data/test/test_exception_handler.rb +0 -124
- data/test/test_extensions.rb +0 -105
- data/test/test_fetch.rb +0 -44
- data/test/test_manager.rb +0 -83
- data/test/test_middleware.rb +0 -135
- data/test/test_processor.rb +0 -160
- data/test/test_redis_connection.rb +0 -97
- data/test/test_retry.rb +0 -306
- data/test/test_scheduled.rb +0 -86
- data/test/test_scheduling.rb +0 -47
- data/test/test_sidekiq.rb +0 -37
- data/test/test_testing.rb +0 -82
- data/test/test_testing_fake.rb +0 -265
- data/test/test_testing_inline.rb +0 -92
- data/test/test_util.rb +0 -18
- data/test/test_web.rb +0 -372
- data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
- data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
- data/web/assets/images/status/active.png +0 -0
- data/web/assets/images/status/idle.png +0 -0
- data/web/assets/javascripts/locales/README.md +0 -27
- data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
- data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.cz.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
- data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
- data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
- data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
- data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
- data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
- data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
- data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
- data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
- data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
- data/web/assets/javascripts/locales/jquery.timeago.zh-CN.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.zh-TW.js +0 -20
- data/web/views/_poll.erb +0 -14
- data/web/views/_workers.erb +0 -29
- data/web/views/index.erb +0 -16
data/lib/sidekiq/rails.rb
CHANGED
@@ -1,19 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Sidekiq
|
2
3
|
def self.hook_rails!
|
3
|
-
if defined?(
|
4
|
-
|
4
|
+
return if defined?(@delay_removed)
|
5
|
+
|
6
|
+
ActiveSupport.on_load(:active_record) do
|
7
|
+
include Sidekiq::Extensions::ActiveRecord
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveSupport.on_load(:action_mailer) do
|
11
|
+
extend Sidekiq::Extensions::ActionMailer
|
5
12
|
end
|
6
13
|
|
7
|
-
|
8
|
-
|
14
|
+
Module.__send__(:include, Sidekiq::Extensions::Klass)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Removes the generic aliases which MAY clash with names of already
|
18
|
+
# created methods by other applications. The methods `sidekiq_delay`,
|
19
|
+
# `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
|
20
|
+
def self.remove_delay!
|
21
|
+
@delay_removed = true
|
22
|
+
|
23
|
+
[Extensions::ActiveRecord,
|
24
|
+
Extensions::ActionMailer,
|
25
|
+
Extensions::Klass].each do |mod|
|
26
|
+
mod.module_eval do
|
27
|
+
remove_method :delay if respond_to?(:delay)
|
28
|
+
remove_method :delay_for if respond_to?(:delay_for)
|
29
|
+
remove_method :delay_until if respond_to?(:delay_until)
|
30
|
+
end
|
9
31
|
end
|
10
32
|
end
|
11
33
|
|
12
34
|
class Rails < ::Rails::Engine
|
13
|
-
|
35
|
+
# We need to setup this up before any application configuration which might
|
36
|
+
# change Sidekiq middleware.
|
37
|
+
#
|
38
|
+
# This hook happens after `Rails::Application` is inherited within
|
39
|
+
# config/application.rb and before config is touched, usually within the
|
40
|
+
# class block. Definitely before config/environments/*.rb and
|
41
|
+
# config/initializers/*.rb.
|
42
|
+
config.before_configuration do
|
43
|
+
if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
|
44
|
+
Sidekiq.server_middleware do |chain|
|
45
|
+
require 'sidekiq/middleware/server/active_record'
|
46
|
+
chain.add Sidekiq::Middleware::Server::ActiveRecord
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
14
50
|
|
15
51
|
initializer 'sidekiq' do
|
16
52
|
Sidekiq.hook_rails!
|
17
53
|
end
|
54
|
+
|
55
|
+
config.after_initialize do
|
56
|
+
# This hook happens after all initializers are run, just before returning
|
57
|
+
# from config/environment.rb back to sidekiq/cli.rb.
|
58
|
+
# We have to add the reloader after initialize to see if cache_classes has
|
59
|
+
# been turned on.
|
60
|
+
#
|
61
|
+
# None of this matters on the client-side, only within the Sidekiq process itself.
|
62
|
+
#
|
63
|
+
Sidekiq.configure_server do |_|
|
64
|
+
if ::Rails::VERSION::MAJOR >= 5
|
65
|
+
# The reloader also takes care of ActiveRecord but is incompatible with
|
66
|
+
# the ActiveRecord middleware so make sure it's not in the chain already.
|
67
|
+
if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
|
68
|
+
raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
|
69
|
+
elsif ::Rails.application.config.cache_classes
|
70
|
+
# The reloader API has proven to be troublesome under load in production.
|
71
|
+
# We won't use it at all when classes are cached, see #3154
|
72
|
+
Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
|
73
|
+
Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
|
74
|
+
else
|
75
|
+
Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
|
76
|
+
Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
|
77
|
+
Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Executor
|
84
|
+
def initialize(app = ::Rails.application)
|
85
|
+
@app = app
|
86
|
+
end
|
87
|
+
|
88
|
+
def call
|
89
|
+
@app.executor.wrap do
|
90
|
+
yield
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect
|
95
|
+
"#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Reloader
|
100
|
+
def initialize(app = ::Rails.application)
|
101
|
+
@app = app
|
102
|
+
end
|
103
|
+
|
104
|
+
def call
|
105
|
+
@app.reloader.wrap do
|
106
|
+
yield
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def inspect
|
111
|
+
"#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
module PsychAutoload
|
116
|
+
def resolve_class(klass_name)
|
117
|
+
klass_name && klass_name.constantize
|
118
|
+
rescue NameError
|
119
|
+
super
|
120
|
+
end
|
121
|
+
end
|
18
122
|
end if defined?(::Rails)
|
19
123
|
end
|
@@ -1,58 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'connection_pool'
|
2
3
|
require 'redis'
|
4
|
+
require 'uri'
|
3
5
|
|
4
6
|
module Sidekiq
|
5
7
|
class RedisConnection
|
6
8
|
class << self
|
7
9
|
|
8
10
|
def create(options={})
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
options = options.symbolize_keys
|
12
|
+
|
13
|
+
options[:url] ||= determine_redis_provider
|
14
|
+
|
15
|
+
size = options[:size] || (Sidekiq.server? ? (Sidekiq.options[:concurrency] + 5) : 5)
|
13
16
|
|
14
|
-
|
17
|
+
verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
|
18
|
+
|
19
|
+
pool_timeout = options[:pool_timeout] || 1
|
20
|
+
log_info(options)
|
15
21
|
|
16
22
|
ConnectionPool.new(:timeout => pool_timeout, :size => size) do
|
17
|
-
build_client(
|
23
|
+
build_client(options)
|
18
24
|
end
|
19
25
|
end
|
20
26
|
|
21
27
|
private
|
22
28
|
|
23
|
-
|
24
|
-
|
29
|
+
# Sidekiq needs a lot of concurrent Redis connections.
|
30
|
+
#
|
31
|
+
# We need a connection for each Processor.
|
32
|
+
# We need a connection for Pro's real-time change listener
|
33
|
+
# We need a connection to various features to call Redis every few seconds:
|
34
|
+
# - the process heartbeat.
|
35
|
+
# - enterprise's leader election
|
36
|
+
# - enterprise's cron support
|
37
|
+
def verify_sizing(size, concurrency)
|
38
|
+
raise ArgumentError, "Your Redis connection pool is too small for Sidekiq to work. Your pool has #{size} connections but really needs to have at least #{concurrency + 2}" if size <= concurrency
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_client(options)
|
42
|
+
namespace = options[:namespace]
|
43
|
+
|
44
|
+
client = Redis.new client_opts(options)
|
25
45
|
if namespace
|
26
|
-
|
27
|
-
|
46
|
+
begin
|
47
|
+
require 'redis/namespace'
|
48
|
+
Redis::Namespace.new(namespace, :redis => client)
|
49
|
+
rescue LoadError
|
50
|
+
Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
|
51
|
+
"Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
|
52
|
+
exit(-127)
|
53
|
+
end
|
28
54
|
else
|
29
55
|
client
|
30
56
|
end
|
31
57
|
end
|
32
58
|
|
33
|
-
def client_opts(
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
59
|
+
def client_opts(options)
|
60
|
+
opts = options.dup
|
61
|
+
if opts[:namespace]
|
62
|
+
opts.delete(:namespace)
|
63
|
+
end
|
64
|
+
|
65
|
+
if opts[:network_timeout]
|
66
|
+
opts[:timeout] = opts[:network_timeout]
|
67
|
+
opts.delete(:network_timeout)
|
38
68
|
end
|
69
|
+
|
70
|
+
opts[:driver] ||= 'ruby'
|
71
|
+
|
72
|
+
# Issue #3303, redis-rb will silently retry an operation.
|
73
|
+
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
74
|
+
# is performed twice but I believe this is much, much rarer
|
75
|
+
# than the reconnect silently fixing a problem; we keep it
|
76
|
+
# on by default.
|
77
|
+
opts[:reconnect_attempts] ||= 1
|
78
|
+
|
79
|
+
opts
|
39
80
|
end
|
40
81
|
|
41
|
-
def log_info(
|
42
|
-
|
43
|
-
|
82
|
+
def log_info(options)
|
83
|
+
# Don't log Redis AUTH password
|
84
|
+
redacted = "REDACTED"
|
85
|
+
scrubbed_options = options.dup
|
86
|
+
if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
|
87
|
+
uri.password = redacted
|
88
|
+
scrubbed_options[:url] = uri.to_s
|
89
|
+
end
|
90
|
+
if scrubbed_options[:password]
|
91
|
+
scrubbed_options[:password] = redacted
|
92
|
+
end
|
44
93
|
if Sidekiq.server?
|
45
|
-
Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION}
|
94
|
+
Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with redis options #{scrubbed_options}")
|
46
95
|
else
|
47
|
-
Sidekiq.logger.
|
96
|
+
Sidekiq.logger.debug("#{Sidekiq::NAME} client with redis options #{scrubbed_options}")
|
48
97
|
end
|
49
98
|
end
|
50
99
|
|
51
100
|
def determine_redis_provider
|
52
|
-
|
53
|
-
return ENV['REDISTOGO_URL'] if ENV['REDISTOGO_URL']
|
54
|
-
provider = ENV['REDIS_PROVIDER'] || 'REDIS_URL'
|
55
|
-
ENV[provider]
|
101
|
+
ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
|
56
102
|
end
|
57
103
|
|
58
104
|
end
|
data/lib/sidekiq/scheduled.rb
CHANGED
@@ -1,75 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'sidekiq'
|
2
3
|
require 'sidekiq/util'
|
3
|
-
require 'sidekiq/
|
4
|
+
require 'sidekiq/api'
|
4
5
|
|
5
6
|
module Sidekiq
|
6
7
|
module Scheduled
|
8
|
+
SETS = %w(retry schedule)
|
7
9
|
|
8
|
-
|
10
|
+
class Enq
|
11
|
+
def enqueue_jobs(now=Time.now.to_f.to_s, sorted_sets=SETS)
|
12
|
+
# A job's "score" in Redis is the time at which it should be processed.
|
13
|
+
# Just check Redis for the set of jobs with a timestamp before now.
|
14
|
+
Sidekiq.redis do |conn|
|
15
|
+
sorted_sets.each do |sorted_set|
|
16
|
+
# Get the next item in the queue if it's score (time to execute) is <= now.
|
17
|
+
# We need to go through the list one at a time to reduce the risk of something
|
18
|
+
# going wrong between the time jobs are popped from the scheduled queue and when
|
19
|
+
# they are pushed onto a work queue and losing the jobs.
|
20
|
+
while job = conn.zrangebyscore(sorted_set, '-inf'.freeze, now, :limit => [0, 1]).first do
|
21
|
+
|
22
|
+
# Pop item off the queue and add it to the work queue. If the job can't be popped from
|
23
|
+
# the queue, it's because another process already popped it so we can move on to the
|
24
|
+
# next one.
|
25
|
+
if conn.zrem(sorted_set, job)
|
26
|
+
Sidekiq::Client.push(Sidekiq.load_json(job))
|
27
|
+
Sidekiq::Logging.logger.debug { "enqueued #{sorted_set}: #{job}" }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
9
34
|
|
10
35
|
##
|
11
|
-
# The Poller checks Redis every N seconds for
|
36
|
+
# The Poller checks Redis every N seconds for jobs in the retry or scheduled
|
12
37
|
# set have passed their timestamp and should be enqueued. If so, it
|
13
|
-
# just pops the
|
14
|
-
# workers can pick it up like any other
|
38
|
+
# just pops the job back onto its original queue so the
|
39
|
+
# workers can pick it up like any other job.
|
15
40
|
class Poller
|
16
41
|
include Util
|
17
|
-
include Actor
|
18
|
-
|
19
|
-
SETS = %w(retry schedule)
|
20
|
-
|
21
|
-
def poll(first_time=false)
|
22
|
-
watchdog('scheduling poller thread died!') do
|
23
|
-
add_jitter if first_time
|
24
|
-
|
25
|
-
begin
|
26
|
-
# A message's "score" in Redis is the time at which it should be processed.
|
27
|
-
# Just check Redis for the set of messages with a timestamp before now.
|
28
|
-
now = Time.now.to_f.to_s
|
29
|
-
Sidekiq.redis do |conn|
|
30
|
-
SETS.each do |sorted_set|
|
31
|
-
# Get the next item in the queue if it's score (time to execute) is <= now.
|
32
|
-
# We need to go through the list one at a time to reduce the risk of something
|
33
|
-
# going wrong between the time jobs are popped from the scheduled queue and when
|
34
|
-
# they are pushed onto a work queue and losing the jobs.
|
35
|
-
while message = conn.zrangebyscore(sorted_set, '-inf', now, :limit => [0, 1]).first do
|
36
|
-
|
37
|
-
# Pop item off the queue and add it to the work queue. If the job can't be popped from
|
38
|
-
# the queue, it's because another process already popped it so we can move on to the
|
39
|
-
# next one.
|
40
|
-
if conn.zrem(sorted_set, message)
|
41
|
-
Sidekiq::Client.push(Sidekiq.load_json(message))
|
42
|
-
logger.debug { "enqueued #{sorted_set}: #{message}" }
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
rescue => ex
|
48
|
-
# Most likely a problem with redis networking.
|
49
|
-
# Punt and try again at the next interval
|
50
|
-
logger.error ex.message
|
51
|
-
logger.error ex.backtrace.first
|
52
|
-
end
|
53
42
|
|
54
|
-
|
43
|
+
INITIAL_WAIT = 10
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
|
47
|
+
@sleeper = ConnectionPool::TimedStack.new
|
48
|
+
@done = false
|
49
|
+
@thread = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Shut down this instance, will pause until the thread is dead.
|
53
|
+
def terminate
|
54
|
+
@done = true
|
55
|
+
if @thread
|
56
|
+
t = @thread
|
57
|
+
@thread = nil
|
58
|
+
@sleeper << 0
|
59
|
+
t.value
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
58
|
-
|
63
|
+
def start
|
64
|
+
@thread ||= safe_thread("scheduler") do
|
65
|
+
initial_wait
|
59
66
|
|
60
|
-
|
61
|
-
|
67
|
+
while !@done
|
68
|
+
enqueue
|
69
|
+
wait
|
70
|
+
end
|
71
|
+
Sidekiq.logger.info("Scheduler exiting...")
|
72
|
+
end
|
62
73
|
end
|
63
74
|
|
64
|
-
def
|
75
|
+
def enqueue
|
65
76
|
begin
|
66
|
-
|
67
|
-
rescue
|
68
|
-
#
|
69
|
-
#
|
77
|
+
@enq.enqueue_jobs
|
78
|
+
rescue => ex
|
79
|
+
# Most likely a problem with redis networking.
|
80
|
+
# Punt and try again at the next interval
|
81
|
+
logger.error ex.message
|
82
|
+
ex.backtrace.each do |bt|
|
83
|
+
logger.error(bt)
|
84
|
+
end
|
70
85
|
end
|
71
86
|
end
|
72
87
|
|
88
|
+
private
|
89
|
+
|
90
|
+
def wait
|
91
|
+
@sleeper.pop(random_poll_interval)
|
92
|
+
rescue Timeout::Error
|
93
|
+
# expected
|
94
|
+
rescue => ex
|
95
|
+
# if poll_interval_average hasn't been calculated yet, we can
|
96
|
+
# raise an error trying to reach Redis.
|
97
|
+
logger.error ex.message
|
98
|
+
logger.error ex.backtrace.first
|
99
|
+
sleep 5
|
100
|
+
end
|
101
|
+
|
102
|
+
# Calculates a random interval that is ±50% the desired average.
|
103
|
+
def random_poll_interval
|
104
|
+
poll_interval_average * rand + poll_interval_average.to_f / 2
|
105
|
+
end
|
106
|
+
|
107
|
+
# We do our best to tune the poll interval to the size of the active Sidekiq
|
108
|
+
# cluster. If you have 30 processes and poll every 15 seconds, that means one
|
109
|
+
# Sidekiq is checking Redis every 0.5 seconds - way too often for most people
|
110
|
+
# and really bad if the retry or scheduled sets are large.
|
111
|
+
#
|
112
|
+
# Instead try to avoid polling more than once every 15 seconds. If you have
|
113
|
+
# 30 Sidekiq processes, we'll poll every 30 * 15 or 450 seconds.
|
114
|
+
# To keep things statistically random, we'll sleep a random amount between
|
115
|
+
# 225 and 675 seconds for each poll or 450 seconds on average. Otherwise restarting
|
116
|
+
# all your Sidekiq processes at the same time will lead to them all polling at
|
117
|
+
# the same time: the thundering herd problem.
|
118
|
+
#
|
119
|
+
# We only do this if poll_interval_average is unset (the default).
|
120
|
+
def poll_interval_average
|
121
|
+
Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval
|
122
|
+
end
|
123
|
+
|
124
|
+
# Calculates an average poll interval based on the number of known Sidekiq processes.
|
125
|
+
# This minimizes a single point of failure by dispersing check-ins but without taxing
|
126
|
+
# Redis if you run many Sidekiq processes.
|
127
|
+
def scaled_poll_interval
|
128
|
+
pcount = Sidekiq::ProcessSet.new.size
|
129
|
+
pcount = 1 if pcount == 0
|
130
|
+
pcount * Sidekiq.options[:average_scheduled_poll_interval]
|
131
|
+
end
|
132
|
+
|
133
|
+
def initial_wait
|
134
|
+
# Have all processes sleep between 5-15 seconds. 10 seconds
|
135
|
+
# to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
|
136
|
+
# of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
|
137
|
+
total = 0
|
138
|
+
total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average]
|
139
|
+
total += (5 * rand)
|
140
|
+
|
141
|
+
@sleeper.pop(total)
|
142
|
+
rescue Timeout::Error
|
143
|
+
end
|
144
|
+
|
73
145
|
end
|
74
146
|
end
|
75
147
|
end
|