sidekiq 3.5.4 → 5.2.7
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 +5 -5
- data/.circleci/config.yml +61 -0
- data/{Contributing.md → .github/contributing.md} +0 -0
- data/.github/issue_template.md +11 -0
- data/.gitignore +3 -0
- data/.travis.yml +5 -10
- data/4.0-Upgrade.md +53 -0
- data/5.0-Upgrade.md +56 -0
- data/COMM-LICENSE +13 -11
- data/Changes.md +376 -1
- data/Ent-Changes.md +201 -2
- data/Gemfile +14 -18
- data/LICENSE +1 -1
- data/Pro-3.0-Upgrade.md +44 -0
- data/Pro-4.0-Upgrade.md +35 -0
- data/Pro-Changes.md +307 -2
- data/README.md +34 -22
- data/Rakefile +3 -3
- data/bin/sidekiq +0 -1
- data/bin/sidekiqctl +13 -86
- data/bin/sidekiqload +23 -27
- data/code_of_conduct.md +50 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +3 -3
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +6 -6
- data/lib/sidekiq.rb +72 -25
- data/lib/sidekiq/api.rb +206 -73
- data/lib/sidekiq/cli.rb +145 -101
- data/lib/sidekiq/client.rb +42 -36
- data/lib/sidekiq/core_ext.rb +1 -105
- data/lib/sidekiq/ctl.rb +221 -0
- data/lib/sidekiq/delay.rb +42 -0
- data/lib/sidekiq/exception_handler.rb +4 -5
- data/lib/sidekiq/extensions/action_mailer.rb +1 -0
- data/lib/sidekiq/extensions/active_record.rb +1 -0
- data/lib/sidekiq/extensions/class_methods.rb +1 -0
- data/lib/sidekiq/extensions/generic_proxy.rb +8 -1
- data/lib/sidekiq/fetch.rb +36 -111
- data/lib/sidekiq/job_logger.rb +25 -0
- data/lib/sidekiq/job_retry.rb +262 -0
- data/lib/sidekiq/launcher.rb +129 -55
- data/lib/sidekiq/logging.rb +21 -3
- data/lib/sidekiq/manager.rb +83 -182
- data/lib/sidekiq/middleware/chain.rb +1 -0
- data/lib/sidekiq/middleware/i18n.rb +1 -0
- data/lib/sidekiq/middleware/server/active_record.rb +10 -0
- data/lib/sidekiq/paginator.rb +1 -0
- data/lib/sidekiq/processor.rb +221 -103
- data/lib/sidekiq/rails.rb +47 -27
- data/lib/sidekiq/redis_connection.rb +74 -7
- data/lib/sidekiq/scheduled.rb +87 -28
- data/lib/sidekiq/testing.rb +150 -19
- data/lib/sidekiq/testing/inline.rb +1 -0
- data/lib/sidekiq/util.rb +15 -17
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +120 -184
- data/lib/sidekiq/web/action.rb +89 -0
- data/lib/sidekiq/web/application.rb +353 -0
- data/lib/sidekiq/{web_helpers.rb → web/helpers.rb} +123 -47
- data/lib/sidekiq/web/router.rb +100 -0
- data/lib/sidekiq/worker.rb +135 -18
- data/sidekiq.gemspec +8 -14
- data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
- data/web/assets/javascripts/application.js +24 -20
- data/web/assets/javascripts/dashboard.js +33 -18
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +401 -7
- data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
- data/web/assets/stylesheets/bootstrap.css +4 -8
- data/web/locales/ar.yml +81 -0
- data/web/locales/cs.yml +11 -1
- data/web/locales/de.yml +1 -1
- data/web/locales/en.yml +4 -0
- data/web/locales/es.yml +4 -3
- data/web/locales/fa.yml +80 -0
- data/web/locales/fr.yml +21 -12
- data/web/locales/he.yml +79 -0
- data/web/locales/ja.yml +24 -13
- data/web/locales/ru.yml +3 -0
- data/web/locales/ur.yml +80 -0
- data/web/views/_footer.erb +7 -9
- data/web/views/_job_info.erb +5 -1
- data/web/views/_nav.erb +5 -19
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +18 -9
- data/web/views/dashboard.erb +5 -5
- data/web/views/dead.erb +1 -1
- data/web/views/layout.erb +13 -5
- data/web/views/morgue.erb +16 -12
- data/web/views/queue.erb +12 -11
- data/web/views/queues.erb +5 -3
- data/web/views/retries.erb +19 -13
- data/web/views/retry.erb +2 -2
- data/web/views/scheduled.erb +4 -4
- data/web/views/scheduled_job_info.erb +1 -1
- metadata +45 -227
- data/lib/sidekiq/actor.rb +0 -39
- data/lib/sidekiq/middleware/server/logging.rb +0 -40
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -206
- data/test/config.yml +0 -9
- data/test/env_based_config.yml +0 -11
- data/test/fake_env.rb +0 -0
- data/test/fixtures/en.yml +0 -2
- data/test/helper.rb +0 -49
- data/test/test_api.rb +0 -493
- data/test/test_cli.rb +0 -335
- data/test/test_client.rb +0 -194
- data/test/test_exception_handler.rb +0 -55
- data/test/test_extensions.rb +0 -126
- data/test/test_fetch.rb +0 -104
- data/test/test_logging.rb +0 -34
- data/test/test_manager.rb +0 -168
- data/test/test_middleware.rb +0 -159
- data/test/test_processor.rb +0 -237
- data/test/test_rails.rb +0 -21
- data/test/test_redis_connection.rb +0 -126
- data/test/test_retry.rb +0 -325
- data/test/test_scheduled.rb +0 -114
- data/test/test_scheduling.rb +0 -49
- data/test/test_sidekiq.rb +0 -99
- data/test/test_testing.rb +0 -142
- data/test/test_testing_fake.rb +0 -268
- data/test/test_testing_inline.rb +0 -93
- data/test/test_util.rb +0 -16
- data/test/test_web.rb +0 -608
- data/test/test_web_helpers.rb +0 -53
- 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.cs.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
- 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_js.erb +0 -5
data/lib/sidekiq/rails.rb
CHANGED
@@ -1,38 +1,58 @@
|
|
1
|
-
|
2
|
-
def self.hook_rails!
|
3
|
-
return if defined?(@delay_removed)
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
3
|
+
module Sidekiq
|
4
|
+
class Rails < ::Rails::Engine
|
5
|
+
# We need to setup this up before any application configuration which might
|
6
|
+
# change Sidekiq middleware.
|
7
|
+
#
|
8
|
+
# This hook happens after `Rails::Application` is inherited within
|
9
|
+
# config/application.rb and before config is touched, usually within the
|
10
|
+
# class block. Definitely before config/environments/*.rb and
|
11
|
+
# config/initializers/*.rb.
|
12
|
+
config.before_configuration do
|
13
|
+
if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
|
14
|
+
Sidekiq.server_middleware do |chain|
|
15
|
+
require 'sidekiq/middleware/server/active_record'
|
16
|
+
chain.add Sidekiq::Middleware::Server::ActiveRecord
|
17
|
+
end
|
18
|
+
end
|
7
19
|
end
|
8
20
|
|
9
|
-
|
10
|
-
|
21
|
+
config.after_initialize do
|
22
|
+
# This hook happens after all initializers are run, just before returning
|
23
|
+
# from config/environment.rb back to sidekiq/cli.rb.
|
24
|
+
# We have to add the reloader after initialize to see if cache_classes has
|
25
|
+
# been turned on.
|
26
|
+
#
|
27
|
+
# None of this matters on the client-side, only within the Sidekiq process itself.
|
28
|
+
#
|
29
|
+
Sidekiq.configure_server do |_|
|
30
|
+
if ::Rails::VERSION::MAJOR >= 5
|
31
|
+
Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
|
32
|
+
end
|
33
|
+
end
|
11
34
|
end
|
12
35
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# created methods by other applications. The methods `sidekiq_delay`,
|
18
|
-
# `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
|
19
|
-
def self.remove_delay!
|
20
|
-
@delay_removed = true
|
36
|
+
class Reloader
|
37
|
+
def initialize(app = ::Rails.application)
|
38
|
+
@app = app
|
39
|
+
end
|
21
40
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
remove_method :delay if respond_to?(:delay)
|
27
|
-
remove_method :delay_for if respond_to?(:delay_for)
|
28
|
-
remove_method :delay_until if respond_to?(:delay_until)
|
41
|
+
def call
|
42
|
+
@app.reloader.wrap do
|
43
|
+
yield
|
44
|
+
end
|
29
45
|
end
|
30
|
-
end
|
31
|
-
end
|
32
46
|
|
33
|
-
|
34
|
-
|
35
|
-
|
47
|
+
def inspect
|
48
|
+
"#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
|
49
|
+
end
|
36
50
|
end
|
37
51
|
end if defined?(::Rails)
|
38
52
|
end
|
53
|
+
|
54
|
+
if defined?(::Rails) && ::Rails::VERSION::MAJOR < 4
|
55
|
+
$stderr.puts("**************************************************")
|
56
|
+
$stderr.puts("⛔️ WARNING: Sidekiq server is no longer supported by Rails 3.2 - please ensure your server/workers are updated")
|
57
|
+
$stderr.puts("**************************************************")
|
58
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'connection_pool'
|
2
3
|
require 'redis'
|
3
4
|
require 'uri'
|
@@ -7,12 +8,26 @@ module Sidekiq
|
|
7
8
|
class << self
|
8
9
|
|
9
10
|
def create(options={})
|
11
|
+
options.keys.each do |key|
|
12
|
+
options[key.to_sym] = options.delete(key)
|
13
|
+
end
|
14
|
+
|
15
|
+
options[:id] = "Sidekiq-#{Sidekiq.server? ? "server" : "client"}-PID-#{$$}" if !options.has_key?(:id)
|
10
16
|
options[:url] ||= determine_redis_provider
|
11
17
|
|
12
|
-
|
13
|
-
|
14
|
-
|
18
|
+
size = if options[:size]
|
19
|
+
options[:size]
|
20
|
+
elsif Sidekiq.server?
|
21
|
+
Sidekiq.options[:concurrency] + 5
|
22
|
+
elsif ENV['RAILS_MAX_THREADS']
|
23
|
+
Integer(ENV['RAILS_MAX_THREADS'])
|
24
|
+
else
|
25
|
+
5
|
26
|
+
end
|
27
|
+
|
28
|
+
verify_sizing(size, Sidekiq.options[:concurrency]) if Sidekiq.server?
|
15
29
|
|
30
|
+
pool_timeout = options[:pool_timeout] || 1
|
16
31
|
log_info(options)
|
17
32
|
|
18
33
|
ConnectionPool.new(:timeout => pool_timeout, :size => size) do
|
@@ -22,13 +37,31 @@ module Sidekiq
|
|
22
37
|
|
23
38
|
private
|
24
39
|
|
40
|
+
# Sidekiq needs a lot of concurrent Redis connections.
|
41
|
+
#
|
42
|
+
# We need a connection for each Processor.
|
43
|
+
# We need a connection for Pro's real-time change listener
|
44
|
+
# We need a connection to various features to call Redis every few seconds:
|
45
|
+
# - the process heartbeat.
|
46
|
+
# - enterprise's leader election
|
47
|
+
# - enterprise's cron support
|
48
|
+
def verify_sizing(size, concurrency)
|
49
|
+
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
|
50
|
+
end
|
51
|
+
|
25
52
|
def build_client(options)
|
26
53
|
namespace = options[:namespace]
|
27
54
|
|
28
55
|
client = Redis.new client_opts(options)
|
29
56
|
if namespace
|
30
|
-
|
31
|
-
|
57
|
+
begin
|
58
|
+
require 'redis/namespace'
|
59
|
+
Redis::Namespace.new(namespace, :redis => client)
|
60
|
+
rescue LoadError
|
61
|
+
Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
|
62
|
+
"Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
|
63
|
+
exit(-127)
|
64
|
+
end
|
32
65
|
else
|
33
66
|
client
|
34
67
|
end
|
@@ -45,7 +78,14 @@ module Sidekiq
|
|
45
78
|
opts.delete(:network_timeout)
|
46
79
|
end
|
47
80
|
|
48
|
-
opts[:driver]
|
81
|
+
opts[:driver] ||= Redis::Connection.drivers.last || 'ruby'
|
82
|
+
|
83
|
+
# Issue #3303, redis-rb will silently retry an operation.
|
84
|
+
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
85
|
+
# is performed twice but I believe this is much, much rarer
|
86
|
+
# than the reconnect silently fixing a problem; we keep it
|
87
|
+
# on by default.
|
88
|
+
opts[:reconnect_attempts] ||= 1
|
49
89
|
|
50
90
|
opts
|
51
91
|
end
|
@@ -69,7 +109,34 @@ module Sidekiq
|
|
69
109
|
end
|
70
110
|
|
71
111
|
def determine_redis_provider
|
72
|
-
|
112
|
+
# If you have this in your environment:
|
113
|
+
# MY_REDIS_URL=redis://hostname.example.com:1238/4
|
114
|
+
# then set:
|
115
|
+
# REDIS_PROVIDER=MY_REDIS_URL
|
116
|
+
# and Sidekiq will find your custom URL variable with no custom
|
117
|
+
# initialization code at all.
|
118
|
+
p = ENV['REDIS_PROVIDER']
|
119
|
+
if p && p =~ /\:/
|
120
|
+
Sidekiq.logger.error <<-EOM
|
121
|
+
|
122
|
+
#################################################################################
|
123
|
+
|
124
|
+
REDIS_PROVIDER should be set to the **name** of the variable which contains the Redis URL, not a URL itself.
|
125
|
+
Platforms like Heroku sell addons that publish a *_URL variable. You tell Sidekiq with REDIS_PROVIDER, e.g.:
|
126
|
+
|
127
|
+
REDIS_PROVIDER=REDISTOGO_URL
|
128
|
+
REDISTOGO_URL=redis://somehost.example.com:6379/4
|
129
|
+
|
130
|
+
Use REDIS_URL if you wish to point Sidekiq to a URL directly.
|
131
|
+
|
132
|
+
This configuration error will crash starting in Sidekiq 5.3.
|
133
|
+
|
134
|
+
#################################################################################
|
135
|
+
EOM
|
136
|
+
end
|
137
|
+
ENV[
|
138
|
+
ENV['REDIS_PROVIDER'] || 'REDIS_URL'
|
139
|
+
]
|
73
140
|
end
|
74
141
|
|
75
142
|
end
|
data/lib/sidekiq/scheduled.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'sidekiq'
|
2
3
|
require 'sidekiq/util'
|
3
|
-
require 'sidekiq/actor'
|
4
4
|
require 'sidekiq/api'
|
5
5
|
|
6
6
|
module Sidekiq
|
@@ -39,36 +39,92 @@ module Sidekiq
|
|
39
39
|
# workers can pick it up like any other job.
|
40
40
|
class Poller
|
41
41
|
include Util
|
42
|
-
include Actor
|
43
42
|
|
44
43
|
INITIAL_WAIT = 10
|
45
44
|
|
46
45
|
def initialize
|
47
46
|
@enq = (Sidekiq.options[:scheduled_enq] || Sidekiq::Scheduled::Enq).new
|
47
|
+
@sleeper = ConnectionPool::TimedStack.new
|
48
|
+
@done = false
|
49
|
+
@thread = nil
|
48
50
|
end
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def start
|
64
|
+
@thread ||= safe_thread("scheduler") do
|
65
|
+
initial_wait
|
66
|
+
|
67
|
+
while !@done
|
68
|
+
enqueue
|
69
|
+
wait
|
61
70
|
end
|
71
|
+
Sidekiq.logger.info("Scheduler exiting...")
|
72
|
+
end
|
73
|
+
end
|
62
74
|
|
63
|
-
|
75
|
+
def enqueue
|
76
|
+
begin
|
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
|
+
handle_exception(ex)
|
64
83
|
end
|
65
84
|
end
|
66
85
|
|
67
86
|
private
|
68
87
|
|
69
|
-
|
88
|
+
def wait
|
89
|
+
@sleeper.pop(random_poll_interval)
|
90
|
+
rescue Timeout::Error
|
91
|
+
# expected
|
92
|
+
rescue => ex
|
93
|
+
# if poll_interval_average hasn't been calculated yet, we can
|
94
|
+
# raise an error trying to reach Redis.
|
95
|
+
logger.error ex.message
|
96
|
+
handle_exception(ex)
|
97
|
+
sleep 5
|
98
|
+
end
|
99
|
+
|
70
100
|
def random_poll_interval
|
71
|
-
|
101
|
+
# We want one Sidekiq process to schedule jobs every N seconds. We have M processes
|
102
|
+
# and **don't** want to coordinate.
|
103
|
+
#
|
104
|
+
# So in N*M second timespan, we want each process to schedule once. The basic loop is:
|
105
|
+
#
|
106
|
+
# * sleep a random amount within that N*M timespan
|
107
|
+
# * wake up and schedule
|
108
|
+
#
|
109
|
+
# We want to avoid one edge case: imagine a set of 2 processes, scheduling every 5 seconds,
|
110
|
+
# so N*M = 10. Each process decides to randomly sleep 8 seconds, now we've failed to meet
|
111
|
+
# that 5 second average. Thankfully each schedule cycle will sleep randomly so the next
|
112
|
+
# iteration could see each process sleep for 1 second, undercutting our average.
|
113
|
+
#
|
114
|
+
# So below 10 processes, we special case and ensure the processes sleep closer to the average.
|
115
|
+
# In the example above, each process should schedule every 10 seconds on average. We special
|
116
|
+
# case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
|
117
|
+
# As we run more processes, the scheduling interval average will approach an even spread
|
118
|
+
# between 0 and poll interval so we don't need this artifical boost.
|
119
|
+
#
|
120
|
+
if process_count < 10
|
121
|
+
# For small clusters, calculate a random interval that is ±50% the desired average.
|
122
|
+
poll_interval_average * rand + poll_interval_average.to_f / 2
|
123
|
+
else
|
124
|
+
# With 10+ processes, we should have enough randomness to get decent polling
|
125
|
+
# across the entire timespan
|
126
|
+
poll_interval_average * rand
|
127
|
+
end
|
72
128
|
end
|
73
129
|
|
74
130
|
# We do our best to tune the poll interval to the size of the active Sidekiq
|
@@ -83,7 +139,7 @@ module Sidekiq
|
|
83
139
|
# all your Sidekiq processes at the same time will lead to them all polling at
|
84
140
|
# the same time: the thundering herd problem.
|
85
141
|
#
|
86
|
-
# We only do this if
|
142
|
+
# We only do this if poll_interval_average is unset (the default).
|
87
143
|
def poll_interval_average
|
88
144
|
Sidekiq.options[:poll_interval_average] ||= scaled_poll_interval
|
89
145
|
end
|
@@ -92,22 +148,25 @@ module Sidekiq
|
|
92
148
|
# This minimizes a single point of failure by dispersing check-ins but without taxing
|
93
149
|
# Redis if you run many Sidekiq processes.
|
94
150
|
def scaled_poll_interval
|
151
|
+
process_count * Sidekiq.options[:average_scheduled_poll_interval]
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_count
|
95
155
|
pcount = Sidekiq::ProcessSet.new.size
|
96
156
|
pcount = 1 if pcount == 0
|
97
|
-
pcount
|
157
|
+
pcount
|
98
158
|
end
|
99
159
|
|
100
160
|
def initial_wait
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
161
|
+
# Have all processes sleep between 5-15 seconds. 10 seconds
|
162
|
+
# to give time for the heartbeat to register (if the poll interval is going to be calculated by the number
|
163
|
+
# of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
|
164
|
+
total = 0
|
165
|
+
total += INITIAL_WAIT unless Sidekiq.options[:poll_interval_average]
|
166
|
+
total += (5 * rand)
|
167
|
+
|
168
|
+
@sleeper.pop(total)
|
169
|
+
rescue Timeout::Error
|
111
170
|
end
|
112
171
|
|
113
172
|
end
|
data/lib/sidekiq/testing.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'securerandom'
|
2
3
|
require 'sidekiq'
|
3
4
|
|
@@ -54,6 +55,15 @@ module Sidekiq
|
|
54
55
|
yield @server_chain if block_given?
|
55
56
|
@server_chain
|
56
57
|
end
|
58
|
+
|
59
|
+
def constantize(str)
|
60
|
+
names = str.split('::')
|
61
|
+
names.shift if names.empty? || names.first.empty?
|
62
|
+
|
63
|
+
names.inject(Object) do |constant, name|
|
64
|
+
constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
65
|
+
end
|
66
|
+
end
|
57
67
|
end
|
58
68
|
end
|
59
69
|
|
@@ -62,25 +72,128 @@ module Sidekiq
|
|
62
72
|
|
63
73
|
class EmptyQueueError < RuntimeError; end
|
64
74
|
|
65
|
-
|
66
|
-
alias_method :raw_push_real, :raw_push
|
67
|
-
|
75
|
+
module TestingClient
|
68
76
|
def raw_push(payloads)
|
69
77
|
if Sidekiq::Testing.fake?
|
70
78
|
payloads.each do |job|
|
71
|
-
job
|
79
|
+
job = Sidekiq.load_json(Sidekiq.dump_json(job))
|
80
|
+
job.merge!('enqueued_at' => Time.now.to_f) unless job['at']
|
81
|
+
Queues.push(job['queue'], job['class'], job)
|
72
82
|
end
|
73
83
|
true
|
74
84
|
elsif Sidekiq::Testing.inline?
|
75
85
|
payloads.each do |job|
|
76
|
-
job['
|
77
|
-
|
78
|
-
|
79
|
-
klass.
|
86
|
+
klass = Sidekiq::Testing.constantize(job['class'])
|
87
|
+
job['id'] ||= SecureRandom.hex(12)
|
88
|
+
job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
|
89
|
+
klass.process_job(job_hash)
|
80
90
|
end
|
81
91
|
true
|
82
92
|
else
|
83
|
-
|
93
|
+
super
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
Sidekiq::Client.prepend TestingClient
|
99
|
+
|
100
|
+
module Queues
|
101
|
+
##
|
102
|
+
# The Queues class is only for testing the fake queue implementation.
|
103
|
+
# There are 2 data structures involved in tandem. This is due to the
|
104
|
+
# Rspec syntax of change(QueueWorker.jobs, :size). It keeps a reference
|
105
|
+
# to the array. Because the array was dervied from a filter of the total
|
106
|
+
# jobs enqueued, it appeared as though the array didn't change.
|
107
|
+
#
|
108
|
+
# To solve this, we'll keep 2 hashes containing the jobs. One with keys based
|
109
|
+
# on the queue, and another with keys of the worker names, so the array for
|
110
|
+
# QueueWorker.jobs is a straight reference to a real array.
|
111
|
+
#
|
112
|
+
# Queue-based hash:
|
113
|
+
#
|
114
|
+
# {
|
115
|
+
# "default"=>[
|
116
|
+
# {
|
117
|
+
# "class"=>"TestTesting::QueueWorker",
|
118
|
+
# "args"=>[1, 2],
|
119
|
+
# "retry"=>true,
|
120
|
+
# "queue"=>"default",
|
121
|
+
# "jid"=>"abc5b065c5c4b27fc1102833",
|
122
|
+
# "created_at"=>1447445554.419934
|
123
|
+
# }
|
124
|
+
# ]
|
125
|
+
# }
|
126
|
+
#
|
127
|
+
# Worker-based hash:
|
128
|
+
#
|
129
|
+
# {
|
130
|
+
# "TestTesting::QueueWorker"=>[
|
131
|
+
# {
|
132
|
+
# "class"=>"TestTesting::QueueWorker",
|
133
|
+
# "args"=>[1, 2],
|
134
|
+
# "retry"=>true,
|
135
|
+
# "queue"=>"default",
|
136
|
+
# "jid"=>"abc5b065c5c4b27fc1102833",
|
137
|
+
# "created_at"=>1447445554.419934
|
138
|
+
# }
|
139
|
+
# ]
|
140
|
+
# }
|
141
|
+
#
|
142
|
+
# Example:
|
143
|
+
#
|
144
|
+
# require 'sidekiq/testing'
|
145
|
+
#
|
146
|
+
# assert_equal 0, Sidekiq::Queues["default"].size
|
147
|
+
# HardWorker.perform_async(:something)
|
148
|
+
# assert_equal 1, Sidekiq::Queues["default"].size
|
149
|
+
# assert_equal :something, Sidekiq::Queues["default"].first['args'][0]
|
150
|
+
#
|
151
|
+
# You can also clear all workers' jobs:
|
152
|
+
#
|
153
|
+
# assert_equal 0, Sidekiq::Queues["default"].size
|
154
|
+
# HardWorker.perform_async(:something)
|
155
|
+
# Sidekiq::Queues.clear_all
|
156
|
+
# assert_equal 0, Sidekiq::Queues["default"].size
|
157
|
+
#
|
158
|
+
# This can be useful to make sure jobs don't linger between tests:
|
159
|
+
#
|
160
|
+
# RSpec.configure do |config|
|
161
|
+
# config.before(:each) do
|
162
|
+
# Sidekiq::Queues.clear_all
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
class << self
|
167
|
+
def [](queue)
|
168
|
+
jobs_by_queue[queue]
|
169
|
+
end
|
170
|
+
|
171
|
+
def push(queue, klass, job)
|
172
|
+
jobs_by_queue[queue] << job
|
173
|
+
jobs_by_worker[klass] << job
|
174
|
+
end
|
175
|
+
|
176
|
+
def jobs_by_queue
|
177
|
+
@jobs_by_queue ||= Hash.new { |hash, key| hash[key] = [] }
|
178
|
+
end
|
179
|
+
|
180
|
+
def jobs_by_worker
|
181
|
+
@jobs_by_worker ||= Hash.new { |hash, key| hash[key] = [] }
|
182
|
+
end
|
183
|
+
|
184
|
+
def delete_for(jid, queue, klass)
|
185
|
+
jobs_by_queue[queue.to_s].delete_if { |job| job["jid"] == jid }
|
186
|
+
jobs_by_worker[klass].delete_if { |job| job["jid"] == jid }
|
187
|
+
end
|
188
|
+
|
189
|
+
def clear_for(queue, klass)
|
190
|
+
jobs_by_queue[queue].clear
|
191
|
+
jobs_by_worker[klass].clear
|
192
|
+
end
|
193
|
+
|
194
|
+
def clear_all
|
195
|
+
jobs_by_queue.clear
|
196
|
+
jobs_by_worker.clear
|
84
197
|
end
|
85
198
|
end
|
86
199
|
end
|
@@ -143,28 +256,36 @@ module Sidekiq
|
|
143
256
|
#
|
144
257
|
module ClassMethods
|
145
258
|
|
259
|
+
# Queue for this worker
|
260
|
+
def queue
|
261
|
+
self.sidekiq_options["queue"]
|
262
|
+
end
|
263
|
+
|
146
264
|
# Jobs queued for this worker
|
147
265
|
def jobs
|
148
|
-
|
266
|
+
Queues.jobs_by_worker[self.to_s]
|
149
267
|
end
|
150
268
|
|
151
269
|
# Clear all jobs for this worker
|
152
270
|
def clear
|
153
|
-
|
271
|
+
Queues.clear_for(queue, self.to_s)
|
154
272
|
end
|
155
273
|
|
156
274
|
# Drain and run all jobs for this worker
|
157
275
|
def drain
|
158
|
-
while
|
159
|
-
|
276
|
+
while jobs.any?
|
277
|
+
next_job = jobs.first
|
278
|
+
Queues.delete_for(next_job["jid"], next_job["queue"], self.to_s)
|
279
|
+
process_job(next_job)
|
160
280
|
end
|
161
281
|
end
|
162
282
|
|
163
283
|
# Pop out a single job and perform it
|
164
284
|
def perform_one
|
165
285
|
raise(EmptyQueueError, "perform_one called with empty job queue") if jobs.empty?
|
166
|
-
|
167
|
-
|
286
|
+
next_job = jobs.first
|
287
|
+
Queues.delete_for(next_job["jid"], queue, self.to_s)
|
288
|
+
process_job(next_job)
|
168
289
|
end
|
169
290
|
|
170
291
|
def process_job(job)
|
@@ -183,20 +304,30 @@ module Sidekiq
|
|
183
304
|
|
184
305
|
class << self
|
185
306
|
def jobs # :nodoc:
|
186
|
-
|
307
|
+
Queues.jobs_by_queue.values.flatten
|
187
308
|
end
|
188
309
|
|
189
310
|
# Clear all queued jobs across all workers
|
190
311
|
def clear_all
|
191
|
-
|
312
|
+
Queues.clear_all
|
192
313
|
end
|
193
314
|
|
194
315
|
# Drain all queued jobs across all workers
|
195
316
|
def drain_all
|
196
|
-
|
197
|
-
jobs.
|
317
|
+
while jobs.any?
|
318
|
+
worker_classes = jobs.map { |job| job["class"] }.uniq
|
319
|
+
|
320
|
+
worker_classes.each do |worker_class|
|
321
|
+
Sidekiq::Testing.constantize(worker_class).drain
|
322
|
+
end
|
198
323
|
end
|
199
324
|
end
|
200
325
|
end
|
201
326
|
end
|
202
327
|
end
|
328
|
+
|
329
|
+
if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test?
|
330
|
+
puts("**************************************************")
|
331
|
+
puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
|
332
|
+
puts("**************************************************")
|
333
|
+
end
|