sidekiq 6.2.2 → 8.1.5
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 +726 -11
- data/LICENSE.txt +9 -0
- data/README.md +70 -39
- data/bin/kiq +17 -0
- data/bin/lint-herb +13 -0
- data/bin/multi_queue_bench +271 -0
- data/bin/sidekiq +4 -9
- data/bin/sidekiqload +214 -115
- data/bin/sidekiqmon +4 -1
- data/bin/webload +69 -0
- data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
- data/lib/generators/sidekiq/job_generator.rb +71 -0
- data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
- 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 +729 -264
- data/lib/sidekiq/capsule.rb +135 -0
- data/lib/sidekiq/cli.rb +124 -100
- data/lib/sidekiq/client.rb +153 -106
- data/lib/sidekiq/component.rb +132 -0
- data/lib/sidekiq/config.rb +320 -0
- data/lib/sidekiq/deploy.rb +64 -0
- data/lib/sidekiq/embedded.rb +64 -0
- data/lib/sidekiq/fetch.rb +27 -26
- data/lib/sidekiq/iterable_job.rb +56 -0
- data/lib/sidekiq/job/interrupt_handler.rb +24 -0
- data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
- data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
- data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
- data/lib/sidekiq/job/iterable.rb +322 -0
- data/lib/sidekiq/job.rb +397 -5
- data/lib/sidekiq/job_logger.rb +23 -32
- data/lib/sidekiq/job_retry.rb +141 -68
- data/lib/sidekiq/job_util.rb +113 -0
- data/lib/sidekiq/launcher.rb +122 -98
- data/lib/sidekiq/loader.rb +57 -0
- data/lib/sidekiq/logger.rb +27 -106
- data/lib/sidekiq/manager.rb +41 -43
- data/lib/sidekiq/metrics/query.rb +184 -0
- data/lib/sidekiq/metrics/shared.rb +109 -0
- data/lib/sidekiq/metrics/tracking.rb +153 -0
- data/lib/sidekiq/middleware/chain.rb +96 -51
- data/lib/sidekiq/middleware/current_attributes.rb +120 -0
- data/lib/sidekiq/middleware/i18n.rb +8 -4
- data/lib/sidekiq/middleware/modules.rb +23 -0
- data/lib/sidekiq/monitor.rb +16 -6
- data/lib/sidekiq/paginator.rb +37 -10
- data/lib/sidekiq/processor.rb +105 -87
- data/lib/sidekiq/profiler.rb +73 -0
- data/lib/sidekiq/rails.rb +49 -36
- data/lib/sidekiq/redis_client_adapter.rb +117 -0
- data/lib/sidekiq/redis_connection.rb +55 -86
- data/lib/sidekiq/ring_buffer.rb +32 -0
- data/lib/sidekiq/scheduled.rb +106 -50
- data/lib/sidekiq/systemd.rb +2 -0
- data/lib/sidekiq/test_api.rb +331 -0
- data/lib/sidekiq/testing/inline.rb +2 -30
- data/lib/sidekiq/testing.rb +2 -342
- data/lib/sidekiq/transaction_aware_client.rb +59 -0
- data/lib/sidekiq/tui/controls.rb +53 -0
- data/lib/sidekiq/tui/filtering.rb +53 -0
- data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
- data/lib/sidekiq/tui/tabs/busy.rb +118 -0
- data/lib/sidekiq/tui/tabs/dead.rb +19 -0
- data/lib/sidekiq/tui/tabs/home.rb +144 -0
- data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
- data/lib/sidekiq/tui/tabs/queues.rb +95 -0
- data/lib/sidekiq/tui/tabs/retries.rb +19 -0
- data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
- data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
- data/lib/sidekiq/tui/tabs.rb +15 -0
- data/lib/sidekiq/tui.rb +382 -0
- data/lib/sidekiq/version.rb +6 -1
- data/lib/sidekiq/web/action.rb +149 -64
- data/lib/sidekiq/web/application.rb +376 -268
- data/lib/sidekiq/web/config.rb +117 -0
- data/lib/sidekiq/web/helpers.rb +213 -87
- data/lib/sidekiq/web/router.rb +61 -74
- data/lib/sidekiq/web.rb +71 -100
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +95 -196
- data/sidekiq.gemspec +14 -11
- data/web/assets/images/logo.png +0 -0
- data/web/assets/images/status.png +0 -0
- data/web/assets/javascripts/application.js +171 -57
- data/web/assets/javascripts/base-charts.js +120 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
- data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
- data/web/assets/javascripts/dashboard-charts.js +194 -0
- data/web/assets/javascripts/dashboard.js +41 -274
- data/web/assets/javascripts/metrics.js +280 -0
- data/web/assets/stylesheets/style.css +776 -0
- data/web/locales/ar.yml +72 -70
- data/web/locales/cs.yml +64 -62
- data/web/locales/da.yml +62 -53
- data/web/locales/de.yml +67 -65
- data/web/locales/el.yml +45 -24
- data/web/locales/en.yml +93 -69
- data/web/locales/es.yml +91 -68
- data/web/locales/fa.yml +67 -65
- data/web/locales/fr.yml +82 -67
- data/web/locales/gd.yml +110 -0
- data/web/locales/he.yml +67 -64
- data/web/locales/hi.yml +61 -59
- data/web/locales/it.yml +94 -54
- data/web/locales/ja.yml +74 -68
- data/web/locales/ko.yml +54 -52
- data/web/locales/lt.yml +68 -66
- data/web/locales/nb.yml +63 -61
- data/web/locales/nl.yml +54 -52
- data/web/locales/pl.yml +47 -45
- data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
- data/web/locales/pt.yml +53 -51
- data/web/locales/ru.yml +69 -66
- data/web/locales/sv.yml +55 -53
- data/web/locales/ta.yml +62 -60
- data/web/locales/tr.yml +102 -0
- data/web/locales/uk.yml +87 -61
- data/web/locales/ur.yml +66 -64
- data/web/locales/vi.yml +69 -67
- data/web/locales/zh-CN.yml +107 -0
- data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
- 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.html.erb +40 -0
- data/web/views/busy.html.erb +151 -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 +130 -61
- 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/util.rb +0 -95
- data/lib/sidekiq/web/csrf_protection.rb +0 -180
- data/lib/sidekiq/worker.rb +0 -244
- data/web/assets/stylesheets/application-dark.css +0 -147
- data/web/assets/stylesheets/application-rtl.css +0 -246
- data/web/assets/stylesheets/application.css +0 -1053
- data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
- data/web/assets/stylesheets/bootstrap.css +0 -5
- data/web/locales/zh-cn.yml +0 -68
- data/web/views/_footer.erb +0 -20
- data/web/views/_job_info.erb +0 -89
- data/web/views/_nav.erb +0 -52
- data/web/views/_paging.erb +0 -23
- data/web/views/_poll_link.erb +0 -7
- data/web/views/_status.erb +0 -4
- data/web/views/_summary.erb +0 -40
- data/web/views/busy.erb +0 -132
- data/web/views/dashboard.erb +0 -83
- data/web/views/dead.erb +0 -34
- data/web/views/layout.erb +0 -42
- data/web/views/morgue.erb +0 -78
- data/web/views/queue.erb +0 -55
- data/web/views/queues.erb +0 -38
- data/web/views/retries.erb +0 -83
- data/web/views/retry.erb +0 -34
- data/web/views/scheduled.erb +0 -57
|
@@ -1,121 +1,92 @@
|
|
|
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
|
-
symbolized_options = options
|
|
11
|
+
symbolized_options = deep_symbolize_keys(options)
|
|
12
|
+
symbolized_options[:url] ||= determine_redis_provider
|
|
13
|
+
symbolized_options[:password] = wrap(symbolized_options[:password]) if symbolized_options.key?(:password)
|
|
14
|
+
symbolized_options[:sentinel_password] = wrap(symbolized_options[:sentinel_password]) if symbolized_options.key?(:sentinel_password)
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
end
|
|
16
|
+
logger = symbolized_options.delete(:logger)
|
|
17
|
+
logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
|
|
16
18
|
|
|
17
|
-
|
|
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
|
|
19
|
+
raise "Sidekiq 7+ does not support Redis protocol 2" if symbolized_options[:protocol] == 2
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
safe = !!symbolized_options.delete(:cluster_safe)
|
|
22
|
+
raise ":nodes not allowed, Sidekiq is not safe to run on Redis Cluster" if !safe && symbolized_options.key?(:nodes)
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
size = symbolized_options.delete(:size) || 5
|
|
25
|
+
pool_timeout = symbolized_options.delete(:pool_timeout) || 1
|
|
26
|
+
symbolized_options.delete(:pool_name)
|
|
33
27
|
|
|
28
|
+
# Default timeout in redis-client is 1 second, which can be too aggressive
|
|
29
|
+
# if the Sidekiq process is CPU-bound. With 10-15 threads and a thread quantum of 100ms,
|
|
30
|
+
# it can be easy to get the occasional ReadTimeoutError. You can still provide
|
|
31
|
+
# a smaller timeout explicitly:
|
|
32
|
+
# config.redis = { url: "...", timeout: 1 }
|
|
33
|
+
symbolized_options[:timeout] ||= 3
|
|
34
|
+
|
|
35
|
+
redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
|
|
34
36
|
ConnectionPool.new(timeout: pool_timeout, size: size) do
|
|
35
|
-
|
|
37
|
+
redis_config.new_client
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
43
|
+
# Wrap hard-coded passwords in a Proc to avoid logging the value
|
|
44
|
+
def wrap(pwd)
|
|
45
|
+
if pwd.is_a?(String)
|
|
46
|
+
->(username) { pwd }
|
|
66
47
|
else
|
|
67
|
-
|
|
48
|
+
pwd
|
|
68
49
|
end
|
|
69
50
|
end
|
|
70
51
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
52
|
+
def deep_symbolize_keys(object)
|
|
53
|
+
case object
|
|
54
|
+
when Hash
|
|
55
|
+
object.each_with_object({}) do |(key, value), result|
|
|
56
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
|
57
|
+
end
|
|
58
|
+
when Array
|
|
59
|
+
object.map { |e| deep_symbolize_keys(e) }
|
|
60
|
+
else
|
|
61
|
+
object
|
|
80
62
|
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
63
|
end
|
|
93
64
|
|
|
94
|
-
def
|
|
65
|
+
def scrub(options)
|
|
95
66
|
redacted = "REDACTED"
|
|
96
67
|
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
keys = options.keys
|
|
102
|
-
keys.delete(:ssl_params)
|
|
68
|
+
# Deep clone so we can muck with these options all we want and exclude
|
|
69
|
+
# params from dump-and-load that may contain objects that Marshal is
|
|
70
|
+
# unable to safely dump.
|
|
71
|
+
keys = options.keys - [:logger, :ssl_params, :password, :sentinel_password]
|
|
103
72
|
scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
|
|
104
73
|
if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
|
|
105
74
|
uri.password = redacted
|
|
106
75
|
scrubbed_options[:url] = uri.to_s
|
|
107
76
|
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
77
|
+
scrubbed_options[:password] = redacted if options.key?(:password)
|
|
78
|
+
scrubbed_options[:sentinel_password] = redacted if options.key?(:sentinel_password)
|
|
111
79
|
scrubbed_options[:sentinels]&.each do |sentinel|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
80
|
+
if sentinel.is_a?(String)
|
|
81
|
+
if (uri = URI(sentinel)) && uri.password
|
|
82
|
+
uri.password = redacted
|
|
83
|
+
sentinel.replace(uri.to_s)
|
|
84
|
+
end
|
|
85
|
+
elsif sentinel[:password]
|
|
86
|
+
sentinel[:password] = redacted
|
|
87
|
+
end
|
|
118
88
|
end
|
|
89
|
+
scrubbed_options
|
|
119
90
|
end
|
|
120
91
|
|
|
121
92
|
def determine_redis_provider
|
|
@@ -137,9 +108,7 @@ module Sidekiq
|
|
|
137
108
|
EOM
|
|
138
109
|
end
|
|
139
110
|
|
|
140
|
-
ENV[
|
|
141
|
-
p || "REDIS_URL"
|
|
142
|
-
]
|
|
111
|
+
ENV[p.to_s] || ENV["REDIS_URL"]
|
|
143
112
|
end
|
|
144
113
|
end
|
|
145
114
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Sidekiq
|
|
6
|
+
class RingBuffer
|
|
7
|
+
include Enumerable
|
|
8
|
+
extend Forwardable
|
|
9
|
+
|
|
10
|
+
def_delegators :@buf, :[], :each, :size
|
|
11
|
+
|
|
12
|
+
def initialize(size, default = 0)
|
|
13
|
+
@size = size
|
|
14
|
+
@buf = Array.new(size, default)
|
|
15
|
+
@index = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def <<(element)
|
|
19
|
+
@buf[@index % @size] = element
|
|
20
|
+
@index += 1
|
|
21
|
+
element
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def buffer
|
|
25
|
+
@buf
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def reset(default = 0)
|
|
29
|
+
@buf.fill(default)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/sidekiq/scheduled.rb
CHANGED
|
@@ -1,37 +1,66 @@
|
|
|
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
|
|
12
|
-
|
|
11
|
+
include Sidekiq::Component
|
|
12
|
+
|
|
13
|
+
LUA_ZPOPBYSCORE = <<~LUA
|
|
14
|
+
local key, now = KEYS[1], ARGV[1]
|
|
15
|
+
local jobs = redis.call("zrange", key, "-inf", now, "byscore", "limit", 0, 1)
|
|
16
|
+
if jobs[1] then
|
|
17
|
+
redis.call("zrem", key, jobs[1])
|
|
18
|
+
return jobs[1]
|
|
19
|
+
end
|
|
20
|
+
LUA
|
|
21
|
+
|
|
22
|
+
def initialize(container)
|
|
23
|
+
@config = container
|
|
24
|
+
@client = Sidekiq::Client.new(config: container)
|
|
25
|
+
@done = false
|
|
26
|
+
@lua_zpopbyscore_sha = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def enqueue_jobs(sorted_sets = SETS)
|
|
13
30
|
# A job's "score" in Redis is the time at which it should be processed.
|
|
14
31
|
# Just check Redis for the set of jobs with a timestamp before now.
|
|
15
|
-
|
|
32
|
+
redis do |conn|
|
|
16
33
|
sorted_sets.each do |sorted_set|
|
|
17
|
-
# Get next
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# the queue, it's because another process already popped it so we can move on to the
|
|
25
|
-
# next one.
|
|
26
|
-
if conn.zrem(sorted_set, job)
|
|
27
|
-
Sidekiq::Client.push(Sidekiq.load_json(job))
|
|
28
|
-
Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
|
|
29
|
-
end
|
|
30
|
-
end
|
|
34
|
+
# Get next item in the queue with score (time to execute) <= now.
|
|
35
|
+
# We need to go through the list one at a time to reduce the risk of something
|
|
36
|
+
# going wrong between the time jobs are popped from the scheduled queue and when
|
|
37
|
+
# they are pushed onto a work queue and losing the jobs.
|
|
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}" }
|
|
31
41
|
end
|
|
32
42
|
end
|
|
33
43
|
end
|
|
34
44
|
end
|
|
45
|
+
|
|
46
|
+
def terminate
|
|
47
|
+
@done = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def zpopbyscore(conn, keys: nil, argv: nil)
|
|
53
|
+
if @lua_zpopbyscore_sha.nil?
|
|
54
|
+
@lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
conn.call("EVALSHA", @lua_zpopbyscore_sha, keys.size, *keys, *argv)
|
|
58
|
+
rescue RedisClient::CommandError => e
|
|
59
|
+
raise unless e.message.start_with?("NOSCRIPT")
|
|
60
|
+
|
|
61
|
+
@lua_zpopbyscore_sha = nil
|
|
62
|
+
retry
|
|
63
|
+
end
|
|
35
64
|
end
|
|
36
65
|
|
|
37
66
|
##
|
|
@@ -40,27 +69,28 @@ module Sidekiq
|
|
|
40
69
|
# just pops the job back onto its original queue so the
|
|
41
70
|
# workers can pick it up like any other job.
|
|
42
71
|
class Poller
|
|
43
|
-
include
|
|
72
|
+
include Sidekiq::Component
|
|
44
73
|
|
|
45
74
|
INITIAL_WAIT = 10
|
|
75
|
+
attr_accessor :rnd
|
|
46
76
|
|
|
47
|
-
def initialize
|
|
48
|
-
@
|
|
77
|
+
def initialize(config)
|
|
78
|
+
@config = config
|
|
79
|
+
@enq = (config[:scheduled_enq] || Sidekiq::Scheduled::Enq).new(config)
|
|
49
80
|
@sleeper = ConnectionPool::TimedStack.new
|
|
50
81
|
@done = false
|
|
51
82
|
@thread = nil
|
|
52
83
|
@count_calls = 0
|
|
84
|
+
@rnd = Random.new
|
|
53
85
|
end
|
|
54
86
|
|
|
55
87
|
# Shut down this instance, will pause until the thread is dead.
|
|
56
88
|
def terminate
|
|
57
89
|
@done = true
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
t.value
|
|
63
|
-
end
|
|
90
|
+
@enq.terminate
|
|
91
|
+
|
|
92
|
+
@sleeper << 0
|
|
93
|
+
@thread&.value
|
|
64
94
|
end
|
|
65
95
|
|
|
66
96
|
def start
|
|
@@ -71,7 +101,7 @@ module Sidekiq
|
|
|
71
101
|
enqueue
|
|
72
102
|
wait
|
|
73
103
|
end
|
|
74
|
-
|
|
104
|
+
logger.info("Scheduler exiting...")
|
|
75
105
|
}
|
|
76
106
|
end
|
|
77
107
|
|
|
@@ -87,9 +117,7 @@ module Sidekiq
|
|
|
87
117
|
private
|
|
88
118
|
|
|
89
119
|
def wait
|
|
90
|
-
@sleeper.pop(random_poll_interval)
|
|
91
|
-
rescue Timeout::Error
|
|
92
|
-
# expected
|
|
120
|
+
@sleeper.pop(timeout: random_poll_interval, exception: false)
|
|
93
121
|
rescue => ex
|
|
94
122
|
# if poll_interval_average hasn't been calculated yet, we can
|
|
95
123
|
# raise an error trying to reach Redis.
|
|
@@ -116,15 +144,18 @@ module Sidekiq
|
|
|
116
144
|
# In the example above, each process should schedule every 10 seconds on average. We special
|
|
117
145
|
# case smaller clusters to add 50% so they would sleep somewhere between 5 and 15 seconds.
|
|
118
146
|
# As we run more processes, the scheduling interval average will approach an even spread
|
|
119
|
-
# between 0 and poll interval so we don't need this
|
|
147
|
+
# between 0 and poll interval so we don't need this artificial boost.
|
|
120
148
|
#
|
|
121
|
-
|
|
149
|
+
count = process_count
|
|
150
|
+
interval = poll_interval_average(count)
|
|
151
|
+
|
|
152
|
+
if count < 10
|
|
122
153
|
# For small clusters, calculate a random interval that is ±50% the desired average.
|
|
123
|
-
|
|
154
|
+
interval * @rnd.rand + interval.to_f / 2
|
|
124
155
|
else
|
|
125
156
|
# With 10+ processes, we should have enough randomness to get decent polling
|
|
126
157
|
# across the entire timespan
|
|
127
|
-
|
|
158
|
+
interval * @rnd.rand * 2
|
|
128
159
|
end
|
|
129
160
|
end
|
|
130
161
|
|
|
@@ -141,38 +172,63 @@ module Sidekiq
|
|
|
141
172
|
# the same time: the thundering herd problem.
|
|
142
173
|
#
|
|
143
174
|
# We only do this if poll_interval_average is unset (the default).
|
|
144
|
-
def poll_interval_average
|
|
145
|
-
|
|
175
|
+
def poll_interval_average(count)
|
|
176
|
+
@config[:poll_interval_average] || scaled_poll_interval(count)
|
|
146
177
|
end
|
|
147
178
|
|
|
148
179
|
# Calculates an average poll interval based on the number of known Sidekiq processes.
|
|
149
180
|
# This minimizes a single point of failure by dispersing check-ins but without taxing
|
|
150
181
|
# Redis if you run many Sidekiq processes.
|
|
151
|
-
def scaled_poll_interval
|
|
152
|
-
process_count *
|
|
182
|
+
def scaled_poll_interval(process_count)
|
|
183
|
+
process_count * @config[:average_scheduled_poll_interval]
|
|
153
184
|
end
|
|
154
185
|
|
|
155
186
|
def process_count
|
|
156
|
-
|
|
157
|
-
# expensive at scale. Cut it down by 90% with this counter.
|
|
158
|
-
# NB: This method is only called by the scheduler thread so we
|
|
159
|
-
# don't need to worry about the thread safety of +=.
|
|
160
|
-
pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
|
|
187
|
+
pcount = Sidekiq.redis { |conn| conn.scard("processes") }
|
|
161
188
|
pcount = 1 if pcount == 0
|
|
162
|
-
@count_calls += 1
|
|
163
189
|
pcount
|
|
164
190
|
end
|
|
165
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", "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
|
+
|
|
166
218
|
def initial_wait
|
|
167
|
-
# Have all processes sleep between 5-15 seconds.
|
|
168
|
-
#
|
|
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
|
|
169
221
|
# of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
|
|
170
222
|
total = 0
|
|
171
|
-
total += INITIAL_WAIT unless
|
|
223
|
+
total += INITIAL_WAIT unless @config[:poll_interval_average]
|
|
172
224
|
total += (5 * rand)
|
|
173
225
|
|
|
174
|
-
@sleeper.pop(total)
|
|
175
|
-
|
|
226
|
+
@sleeper.pop(timeout: total, exception: false)
|
|
227
|
+
ensure
|
|
228
|
+
# periodically clean out the `processes` set in Redis which can collect
|
|
229
|
+
# references to dead processes over time. The process count affects how
|
|
230
|
+
# often we scan for scheduled jobs.
|
|
231
|
+
cleanup
|
|
176
232
|
end
|
|
177
233
|
end
|
|
178
234
|
end
|