sidekiq 6.5.1 → 7.0.9
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 +142 -12
- data/README.md +40 -32
- data/bin/sidekiq +3 -8
- data/bin/sidekiqload +186 -118
- data/bin/sidekiqmon +3 -0
- data/lib/sidekiq/api.rb +226 -139
- data/lib/sidekiq/capsule.rb +127 -0
- data/lib/sidekiq/cli.rb +55 -61
- data/lib/sidekiq/client.rb +31 -18
- data/lib/sidekiq/component.rb +5 -1
- data/lib/sidekiq/config.rb +270 -0
- data/lib/sidekiq/deploy.rb +62 -0
- data/lib/sidekiq/embedded.rb +61 -0
- data/lib/sidekiq/fetch.rb +11 -14
- data/lib/sidekiq/job.rb +375 -10
- data/lib/sidekiq/job_logger.rb +2 -2
- data/lib/sidekiq/job_retry.rb +62 -41
- data/lib/sidekiq/job_util.rb +48 -14
- data/lib/sidekiq/launcher.rb +71 -65
- data/lib/sidekiq/logger.rb +1 -26
- data/lib/sidekiq/manager.rb +9 -11
- data/lib/sidekiq/metrics/query.rb +153 -0
- data/lib/sidekiq/metrics/shared.rb +95 -0
- data/lib/sidekiq/metrics/tracking.rb +136 -0
- data/lib/sidekiq/middleware/chain.rb +84 -48
- data/lib/sidekiq/middleware/current_attributes.rb +12 -17
- data/lib/sidekiq/monitor.rb +17 -4
- data/lib/sidekiq/paginator.rb +9 -1
- data/lib/sidekiq/processor.rb +27 -27
- data/lib/sidekiq/rails.rb +4 -9
- data/lib/sidekiq/redis_client_adapter.rb +8 -47
- data/lib/sidekiq/redis_connection.rb +11 -113
- data/lib/sidekiq/scheduled.rb +60 -33
- data/lib/sidekiq/testing.rb +5 -33
- data/lib/sidekiq/transaction_aware_client.rb +4 -5
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web/action.rb +3 -3
- data/lib/sidekiq/web/application.rb +40 -9
- data/lib/sidekiq/web/csrf_protection.rb +1 -1
- data/lib/sidekiq/web/helpers.rb +32 -18
- data/lib/sidekiq/web.rb +7 -14
- data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
- data/lib/sidekiq.rb +76 -266
- data/sidekiq.gemspec +21 -10
- data/web/assets/javascripts/application.js +19 -1
- 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 +264 -0
- data/web/assets/stylesheets/application-dark.css +4 -0
- data/web/assets/stylesheets/application-rtl.css +2 -91
- data/web/assets/stylesheets/application.css +65 -297
- data/web/locales/ar.yml +70 -70
- data/web/locales/cs.yml +62 -62
- data/web/locales/da.yml +60 -53
- 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/gd.yml +99 -0
- 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 +73 -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 +59 -69
- 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 +43 -16
- data/web/locales/zh-tw.yml +42 -8
- data/web/views/_footer.erb +5 -2
- data/web/views/_job_info.erb +18 -2
- data/web/views/_metrics_period_select.erb +12 -0
- data/web/views/_nav.erb +1 -1
- data/web/views/_paging.erb +2 -0
- data/web/views/_poll_link.erb +1 -1
- data/web/views/busy.erb +43 -27
- data/web/views/dashboard.erb +36 -4
- data/web/views/metrics.erb +82 -0
- data/web/views/metrics_for_job.erb +68 -0
- data/web/views/morgue.erb +5 -9
- data/web/views/queue.erb +15 -15
- data/web/views/queues.erb +3 -1
- data/web/views/retries.erb +5 -9
- data/web/views/scheduled.erb +12 -13
- metadata +60 -27
- data/lib/sidekiq/.DS_Store +0 -0
- data/lib/sidekiq/delay.rb +0 -43
- 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 -367
- /data/{LICENSE → LICENSE.txt} +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "connection_pool"
|
|
4
3
|
require "redis_client"
|
|
5
4
|
require "redis_client/decorator"
|
|
6
|
-
require "uri"
|
|
7
5
|
|
|
8
6
|
module Sidekiq
|
|
9
7
|
class RedisClientAdapter
|
|
@@ -11,37 +9,20 @@ module Sidekiq
|
|
|
11
9
|
CommandError = RedisClient::CommandError
|
|
12
10
|
|
|
13
11
|
module CompatMethods
|
|
12
|
+
# TODO Deprecate and remove this
|
|
14
13
|
def info
|
|
15
14
|
@client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
|
|
16
15
|
end
|
|
17
16
|
|
|
17
|
+
# TODO Deprecate and remove this
|
|
18
18
|
def evalsha(sha, keys, argv)
|
|
19
19
|
@client.call("EVALSHA", sha, keys.size, *keys, *argv)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def brpoplpush(*args)
|
|
23
|
-
@client.blocking_call(false, "BRPOPLPUSH", *args)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def brpop(*args)
|
|
27
|
-
@client.blocking_call(false, "BRPOP", *args)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def set(*args)
|
|
31
|
-
@client.call("SET", *args) { |r| r == "OK" }
|
|
32
|
-
end
|
|
33
|
-
ruby2_keywords :set if respond_to?(:ruby2_keywords, true)
|
|
34
|
-
|
|
35
|
-
def sismember(*args)
|
|
36
|
-
@client.call("SISMEMBER", *args) { |c| c > 0 }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def exists?(key)
|
|
40
|
-
@client.call("EXISTS", key) { |c| c > 0 }
|
|
41
|
-
end
|
|
42
|
-
|
|
43
22
|
private
|
|
44
23
|
|
|
24
|
+
# this allows us to use methods like `conn.hmset(...)` instead of having to use
|
|
25
|
+
# redis-client's native `conn.call("hmset", ...)`
|
|
45
26
|
def method_missing(*args, &block)
|
|
46
27
|
@client.call(*args, *block)
|
|
47
28
|
end
|
|
@@ -55,25 +36,8 @@ module Sidekiq
|
|
|
55
36
|
CompatClient = RedisClient::Decorator.create(CompatMethods)
|
|
56
37
|
|
|
57
38
|
class CompatClient
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
undef_method method
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def disconnect!
|
|
64
|
-
@client.close
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def connection
|
|
68
|
-
{id: @client.id}
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def redis
|
|
72
|
-
self
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def _client
|
|
76
|
-
@client
|
|
39
|
+
def config
|
|
40
|
+
@client.config
|
|
77
41
|
end
|
|
78
42
|
|
|
79
43
|
def message
|
|
@@ -118,9 +82,8 @@ module Sidekiq
|
|
|
118
82
|
opts = options.dup
|
|
119
83
|
|
|
120
84
|
if opts[:namespace]
|
|
121
|
-
|
|
122
|
-
"Either use the redis adapter or remove the namespace."
|
|
123
|
-
Kernel.exit(-127)
|
|
85
|
+
raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature isn't supported by redis-client. " \
|
|
86
|
+
"Either use the redis adapter or remove the namespace."
|
|
124
87
|
end
|
|
125
88
|
|
|
126
89
|
opts.delete(:size)
|
|
@@ -150,5 +113,3 @@ module Sidekiq
|
|
|
150
113
|
end
|
|
151
114
|
end
|
|
152
115
|
end
|
|
153
|
-
|
|
154
|
-
Sidekiq::RedisConnection.adapter = Sidekiq::RedisClientAdapter
|
|
@@ -1,130 +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
|
-
class RedisAdapter
|
|
10
|
-
BaseError = Redis::BaseError
|
|
11
|
-
CommandError = Redis::CommandError
|
|
12
|
-
|
|
13
|
-
def initialize(options)
|
|
14
|
-
warn("Usage of the 'redis' gem within Sidekiq itself is deprecated, Sidekiq 7.0 will only use the new, simpler 'redis-client' gem", caller) if ENV["SIDEKIQ_REDIS_CLIENT"] == "1"
|
|
15
|
-
@options = options
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def new_client
|
|
19
|
-
namespace = @options[:namespace]
|
|
20
|
-
|
|
21
|
-
client = Redis.new client_opts(@options)
|
|
22
|
-
if namespace
|
|
23
|
-
begin
|
|
24
|
-
require "redis/namespace"
|
|
25
|
-
Redis::Namespace.new(namespace, redis: client)
|
|
26
|
-
rescue LoadError
|
|
27
|
-
Sidekiq.logger.error("Your Redis configuration uses the namespace '#{namespace}' but the redis-namespace gem is not included in the Gemfile." \
|
|
28
|
-
"Add the gem to your Gemfile to continue using a namespace. Otherwise, remove the namespace parameter.")
|
|
29
|
-
exit(-127)
|
|
30
|
-
end
|
|
31
|
-
else
|
|
32
|
-
client
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def client_opts(options)
|
|
39
|
-
opts = options.dup
|
|
40
|
-
if opts[:namespace]
|
|
41
|
-
opts.delete(:namespace)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
if opts[:network_timeout]
|
|
45
|
-
opts[:timeout] = opts[:network_timeout]
|
|
46
|
-
opts.delete(:network_timeout)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
opts[:driver] ||= Redis::Connection.drivers.last || "ruby"
|
|
50
|
-
|
|
51
|
-
# Issue #3303, redis-rb will silently retry an operation.
|
|
52
|
-
# This can lead to duplicate jobs if Sidekiq::Client's LPUSH
|
|
53
|
-
# is performed twice but I believe this is much, much rarer
|
|
54
|
-
# than the reconnect silently fixing a problem; we keep it
|
|
55
|
-
# on by default.
|
|
56
|
-
opts[:reconnect_attempts] ||= 1
|
|
57
|
-
|
|
58
|
-
opts
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
@adapter = RedisAdapter
|
|
63
|
-
|
|
64
9
|
class << self
|
|
65
|
-
attr_reader :adapter
|
|
66
|
-
|
|
67
|
-
# RedisConnection.adapter = :redis
|
|
68
|
-
# RedisConnection.adapter = :redis_client
|
|
69
|
-
def adapter=(adapter)
|
|
70
|
-
raise "no" if adapter == self
|
|
71
|
-
result = case adapter
|
|
72
|
-
when :redis
|
|
73
|
-
RedisAdapter
|
|
74
|
-
when Class
|
|
75
|
-
adapter
|
|
76
|
-
else
|
|
77
|
-
require "sidekiq/#{adapter}_adapter"
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
@adapter = result if result
|
|
81
|
-
end
|
|
82
|
-
|
|
83
10
|
def create(options = {})
|
|
84
11
|
symbolized_options = options.transform_keys(&:to_sym)
|
|
12
|
+
symbolized_options[:url] ||= determine_redis_provider
|
|
85
13
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
size = if symbolized_options[:size]
|
|
91
|
-
symbolized_options[:size]
|
|
92
|
-
elsif Sidekiq.server?
|
|
93
|
-
# Give ourselves plenty of connections. pool is lazy
|
|
94
|
-
# so we won't create them until we need them.
|
|
95
|
-
Sidekiq[:concurrency] + 5
|
|
96
|
-
elsif ENV["RAILS_MAX_THREADS"]
|
|
97
|
-
Integer(ENV["RAILS_MAX_THREADS"])
|
|
98
|
-
else
|
|
99
|
-
5
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
verify_sizing(size, Sidekiq[:concurrency]) if Sidekiq.server?
|
|
14
|
+
logger = symbolized_options.delete(:logger)
|
|
15
|
+
logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
|
|
103
16
|
|
|
104
|
-
|
|
105
|
-
|
|
17
|
+
size = symbolized_options.delete(:size) || 5
|
|
18
|
+
pool_timeout = symbolized_options.delete(:pool_timeout) || 1
|
|
19
|
+
pool_name = symbolized_options.delete(:pool_name)
|
|
106
20
|
|
|
107
|
-
redis_config =
|
|
108
|
-
ConnectionPool.new(timeout: pool_timeout, size: size) do
|
|
21
|
+
redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
|
|
22
|
+
ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
|
|
109
23
|
redis_config.new_client
|
|
110
24
|
end
|
|
111
25
|
end
|
|
112
26
|
|
|
113
27
|
private
|
|
114
28
|
|
|
115
|
-
|
|
116
|
-
#
|
|
117
|
-
# We need a connection for each Processor.
|
|
118
|
-
# We need a connection for Pro's real-time change listener
|
|
119
|
-
# We need a connection to various features to call Redis every few seconds:
|
|
120
|
-
# - the process heartbeat.
|
|
121
|
-
# - enterprise's leader election
|
|
122
|
-
# - enterprise's cron support
|
|
123
|
-
def verify_sizing(size, concurrency)
|
|
124
|
-
raise ArgumentError, "Your Redis connection pool is too small for Sidekiq. Your pool has #{size} connections but must have at least #{concurrency + 2}" if size < (concurrency + 2)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def log_info(options)
|
|
29
|
+
def scrub(options)
|
|
128
30
|
redacted = "REDACTED"
|
|
129
31
|
|
|
130
32
|
# Deep clone so we can muck with these options all we want and exclude
|
|
@@ -142,11 +44,7 @@ module Sidekiq
|
|
|
142
44
|
scrubbed_options[:sentinels]&.each do |sentinel|
|
|
143
45
|
sentinel[:password] = redacted if sentinel[:password]
|
|
144
46
|
end
|
|
145
|
-
|
|
146
|
-
Sidekiq.logger.info("Booting Sidekiq #{Sidekiq::VERSION} with #{adapter.name} options #{scrubbed_options}")
|
|
147
|
-
else
|
|
148
|
-
Sidekiq.logger.debug("#{Sidekiq::NAME} client with #{adapter.name} options #{scrubbed_options}")
|
|
149
|
-
end
|
|
47
|
+
scrubbed_options
|
|
150
48
|
end
|
|
151
49
|
|
|
152
50
|
def determine_redis_provider
|
data/lib/sidekiq/scheduled.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "sidekiq"
|
|
4
|
-
require "sidekiq/api"
|
|
5
4
|
require "sidekiq/component"
|
|
6
5
|
|
|
7
6
|
module Sidekiq
|
|
@@ -9,6 +8,8 @@ module Sidekiq
|
|
|
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,7 +19,9 @@ 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)
|
|
22
25
|
@done = false
|
|
23
26
|
@lua_zpopbyscore_sha = nil
|
|
24
27
|
end
|
|
@@ -26,15 +29,15 @@ module Sidekiq
|
|
|
26
29
|
def enqueue_jobs(sorted_sets = SETS)
|
|
27
30
|
# A job's "score" in Redis is the time at which it should be processed.
|
|
28
31
|
# Just check Redis for the set of jobs with a timestamp before now.
|
|
29
|
-
|
|
32
|
+
redis do |conn|
|
|
30
33
|
sorted_sets.each do |sorted_set|
|
|
31
34
|
# Get next item in the queue with score (time to execute) <= now.
|
|
32
35
|
# We need to go through the list one at a time to reduce the risk of something
|
|
33
36
|
# going wrong between the time jobs are popped from the scheduled queue and when
|
|
34
37
|
# they are pushed onto a work queue and losing the jobs.
|
|
35
38
|
while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s]))
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
@client.push(Sidekiq.load_json(job))
|
|
40
|
+
logger.debug { "enqueued #{sorted_set}: #{job}" }
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
end
|
|
@@ -48,12 +51,11 @@ module Sidekiq
|
|
|
48
51
|
|
|
49
52
|
def zpopbyscore(conn, keys: nil, argv: nil)
|
|
50
53
|
if @lua_zpopbyscore_sha.nil?
|
|
51
|
-
|
|
52
|
-
@lua_zpopbyscore_sha = raw_conn.script(:load, LUA_ZPOPBYSCORE)
|
|
54
|
+
@lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
|
-
conn.
|
|
56
|
-
rescue
|
|
57
|
+
conn.call("EVALSHA", @lua_zpopbyscore_sha, keys.size, *keys, *argv)
|
|
58
|
+
rescue RedisClient::CommandError => e
|
|
57
59
|
raise unless e.message.start_with?("NOSCRIPT")
|
|
58
60
|
|
|
59
61
|
@lua_zpopbyscore_sha = nil
|
|
@@ -71,9 +73,9 @@ module Sidekiq
|
|
|
71
73
|
|
|
72
74
|
INITIAL_WAIT = 10
|
|
73
75
|
|
|
74
|
-
def initialize(
|
|
75
|
-
@config =
|
|
76
|
-
@enq = (
|
|
76
|
+
def initialize(config)
|
|
77
|
+
@config = config
|
|
78
|
+
@enq = (config[:scheduled_enq] || Sidekiq::Scheduled::Enq).new(config)
|
|
77
79
|
@sleeper = ConnectionPool::TimedStack.new
|
|
78
80
|
@done = false
|
|
79
81
|
@thread = nil
|
|
@@ -83,14 +85,10 @@ module Sidekiq
|
|
|
83
85
|
# Shut down this instance, will pause until the thread is dead.
|
|
84
86
|
def terminate
|
|
85
87
|
@done = true
|
|
86
|
-
@enq.terminate
|
|
88
|
+
@enq.terminate
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@thread = nil
|
|
91
|
-
@sleeper << 0
|
|
92
|
-
t.value
|
|
93
|
-
end
|
|
90
|
+
@sleeper << 0
|
|
91
|
+
@thread&.value
|
|
94
92
|
end
|
|
95
93
|
|
|
96
94
|
def start
|
|
@@ -148,13 +146,16 @@ module Sidekiq
|
|
|
148
146
|
# As we run more processes, the scheduling interval average will approach an even spread
|
|
149
147
|
# between 0 and poll interval so we don't need this artifical boost.
|
|
150
148
|
#
|
|
151
|
-
|
|
149
|
+
count = process_count
|
|
150
|
+
interval = poll_interval_average(count)
|
|
151
|
+
|
|
152
|
+
if count < 10
|
|
152
153
|
# For small clusters, calculate a random interval that is ±50% the desired average.
|
|
153
|
-
|
|
154
|
+
interval * rand + interval.to_f / 2
|
|
154
155
|
else
|
|
155
156
|
# With 10+ processes, we should have enough randomness to get decent polling
|
|
156
157
|
# across the entire timespan
|
|
157
|
-
|
|
158
|
+
interval * rand
|
|
158
159
|
end
|
|
159
160
|
end
|
|
160
161
|
|
|
@@ -171,31 +172,52 @@ module Sidekiq
|
|
|
171
172
|
# the same time: the thundering herd problem.
|
|
172
173
|
#
|
|
173
174
|
# We only do this if poll_interval_average is unset (the default).
|
|
174
|
-
def poll_interval_average
|
|
175
|
-
@config[:poll_interval_average]
|
|
175
|
+
def poll_interval_average(count)
|
|
176
|
+
@config[:poll_interval_average] || scaled_poll_interval(count)
|
|
176
177
|
end
|
|
177
178
|
|
|
178
179
|
# Calculates an average poll interval based on the number of known Sidekiq processes.
|
|
179
180
|
# This minimizes a single point of failure by dispersing check-ins but without taxing
|
|
180
181
|
# Redis if you run many Sidekiq processes.
|
|
181
|
-
def scaled_poll_interval
|
|
182
|
+
def scaled_poll_interval(process_count)
|
|
182
183
|
process_count * @config[:average_scheduled_poll_interval]
|
|
183
184
|
end
|
|
184
185
|
|
|
185
186
|
def process_count
|
|
186
|
-
|
|
187
|
-
# expensive at scale. Cut it down by 90% with this counter.
|
|
188
|
-
# NB: This method is only called by the scheduler thread so we
|
|
189
|
-
# don't need to worry about the thread safety of +=.
|
|
190
|
-
pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
|
|
187
|
+
pcount = Sidekiq.redis { |conn| conn.scard("processes") }
|
|
191
188
|
pcount = 1 if pcount == 0
|
|
192
|
-
@count_calls += 1
|
|
193
189
|
pcount
|
|
194
190
|
end
|
|
195
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
|
+
|
|
196
218
|
def initial_wait
|
|
197
|
-
# Have all processes sleep between 5-15 seconds.
|
|
198
|
-
#
|
|
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
|
|
199
221
|
# of workers), and 5 random seconds to ensure they don't all hit Redis at the same time.
|
|
200
222
|
total = 0
|
|
201
223
|
total += INITIAL_WAIT unless @config[:poll_interval_average]
|
|
@@ -203,6 +225,11 @@ module Sidekiq
|
|
|
203
225
|
|
|
204
226
|
@sleeper.pop(total)
|
|
205
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
|
|
206
233
|
end
|
|
207
234
|
end
|
|
208
235
|
end
|
data/lib/sidekiq/testing.rb
CHANGED
|
@@ -51,19 +51,10 @@ module Sidekiq
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def server_middleware
|
|
54
|
-
@server_chain ||= Middleware::Chain.new
|
|
54
|
+
@server_chain ||= Middleware::Chain.new(Sidekiq.default_configuration)
|
|
55
55
|
yield @server_chain if block_given?
|
|
56
56
|
@server_chain
|
|
57
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
|
|
67
58
|
end
|
|
68
59
|
end
|
|
69
60
|
|
|
@@ -83,7 +74,7 @@ module Sidekiq
|
|
|
83
74
|
true
|
|
84
75
|
elsif Sidekiq::Testing.inline?
|
|
85
76
|
payloads.each do |job|
|
|
86
|
-
klass =
|
|
77
|
+
klass = Object.const_get(job["class"])
|
|
87
78
|
job["id"] ||= SecureRandom.hex(12)
|
|
88
79
|
job_hash = Sidekiq.load_json(Sidekiq.dump_json(job))
|
|
89
80
|
klass.process_job(job_hash)
|
|
@@ -218,25 +209,9 @@ module Sidekiq
|
|
|
218
209
|
# assert_equal 1, HardJob.jobs.size
|
|
219
210
|
# assert_equal :something, HardJob.jobs[0]['args'][0]
|
|
220
211
|
#
|
|
221
|
-
# assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
|
|
222
|
-
# MyMailer.delay.send_welcome_email('foo@example.com')
|
|
223
|
-
# assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size
|
|
224
|
-
#
|
|
225
212
|
# You can also clear and drain all job types:
|
|
226
213
|
#
|
|
227
|
-
#
|
|
228
|
-
# assert_equal 0, Sidekiq::Extensions::DelayedModel.jobs.size
|
|
229
|
-
#
|
|
230
|
-
# MyMailer.delay.send_welcome_email('foo@example.com')
|
|
231
|
-
# MyModel.delay.do_something_hard
|
|
232
|
-
#
|
|
233
|
-
# assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size
|
|
234
|
-
# assert_equal 1, Sidekiq::Extensions::DelayedModel.jobs.size
|
|
235
|
-
#
|
|
236
|
-
# Sidekiq::Worker.clear_all # or .drain_all
|
|
237
|
-
#
|
|
238
|
-
# assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
|
|
239
|
-
# assert_equal 0, Sidekiq::Extensions::DelayedModel.jobs.size
|
|
214
|
+
# Sidekiq::Job.clear_all # or .drain_all
|
|
240
215
|
#
|
|
241
216
|
# This can be useful to make sure jobs don't linger between tests:
|
|
242
217
|
#
|
|
@@ -318,7 +293,7 @@ module Sidekiq
|
|
|
318
293
|
job_classes = jobs.map { |job| job["class"] }.uniq
|
|
319
294
|
|
|
320
295
|
job_classes.each do |job_class|
|
|
321
|
-
|
|
296
|
+
Object.const_get(job_class).drain
|
|
322
297
|
end
|
|
323
298
|
end
|
|
324
299
|
end
|
|
@@ -329,13 +304,10 @@ module Sidekiq
|
|
|
329
304
|
def jobs_for(klass)
|
|
330
305
|
jobs.select do |job|
|
|
331
306
|
marshalled = job["args"][0]
|
|
332
|
-
marshalled.index(klass.to_s) && YAML.
|
|
307
|
+
marshalled.index(klass.to_s) && YAML.safe_load(marshalled)[0] == klass
|
|
333
308
|
end
|
|
334
309
|
end
|
|
335
310
|
end
|
|
336
|
-
|
|
337
|
-
Sidekiq::Extensions::DelayedMailer.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedMailer)
|
|
338
|
-
Sidekiq::Extensions::DelayedModel.extend(TestingExtensions) if defined?(Sidekiq::Extensions::DelayedModel)
|
|
339
311
|
end
|
|
340
312
|
|
|
341
313
|
if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
|
|
@@ -5,8 +5,8 @@ require "sidekiq/client"
|
|
|
5
5
|
|
|
6
6
|
module Sidekiq
|
|
7
7
|
class TransactionAwareClient
|
|
8
|
-
def initialize(
|
|
9
|
-
@redis_client = Client.new(
|
|
8
|
+
def initialize(pool: nil, config: nil)
|
|
9
|
+
@redis_client = Client.new(pool: pool, config: config)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def push(item)
|
|
@@ -34,11 +34,10 @@ module Sidekiq
|
|
|
34
34
|
begin
|
|
35
35
|
require "after_commit_everywhere"
|
|
36
36
|
rescue LoadError
|
|
37
|
-
|
|
38
|
-
raise
|
|
37
|
+
raise %q(You need to add `gem "after_commit_everywhere"` to your Gemfile to use Sidekiq's transactional client)
|
|
39
38
|
end
|
|
40
39
|
|
|
41
|
-
default_job_options["client_class"] = Sidekiq::TransactionAwareClient
|
|
40
|
+
Sidekiq.default_job_options["client_class"] = Sidekiq::TransactionAwareClient
|
|
42
41
|
Sidekiq::JobUtil::TRANSIENT_ATTRIBUTES << "client_class"
|
|
43
42
|
true
|
|
44
43
|
end
|
data/lib/sidekiq/version.rb
CHANGED
data/lib/sidekiq/web/action.rb
CHANGED
|
@@ -15,11 +15,11 @@ module Sidekiq
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def halt(res)
|
|
18
|
-
throw :halt, [res, {"
|
|
18
|
+
throw :halt, [res, {"content-type" => "text/plain"}, [res.to_s]]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def redirect(location)
|
|
22
|
-
throw :halt, [302, {"
|
|
22
|
+
throw :halt, [302, {"location" => "#{request.base_url}#{location}"}, []]
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def params
|
|
@@ -68,7 +68,7 @@ module Sidekiq
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def json(payload)
|
|
71
|
-
[200, {"
|
|
71
|
+
[200, {"content-type" => "application/json", "cache-control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def initialize(env, block)
|
|
@@ -20,6 +20,12 @@ module Sidekiq
|
|
|
20
20
|
"worker-src 'self'",
|
|
21
21
|
"base-uri 'self'"
|
|
22
22
|
].join("; ").freeze
|
|
23
|
+
METRICS_PERIODS = {
|
|
24
|
+
"1h" => 60,
|
|
25
|
+
"2h" => 120,
|
|
26
|
+
"4h" => 240,
|
|
27
|
+
"8h" => 480
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
def initialize(klass)
|
|
25
31
|
@klass = klass
|
|
@@ -60,17 +66,42 @@ module Sidekiq
|
|
|
60
66
|
erb(:dashboard)
|
|
61
67
|
end
|
|
62
68
|
|
|
69
|
+
get "/metrics" do
|
|
70
|
+
q = Sidekiq::Metrics::Query.new
|
|
71
|
+
@period = h((params[:period] || "")[0..1])
|
|
72
|
+
@periods = METRICS_PERIODS
|
|
73
|
+
minutes = @periods.fetch(@period, @periods.values.first)
|
|
74
|
+
@query_result = q.top_jobs(minutes: minutes)
|
|
75
|
+
erb(:metrics)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
get "/metrics/:name" do
|
|
79
|
+
@name = route_params[:name]
|
|
80
|
+
@period = h((params[:period] || "")[0..1])
|
|
81
|
+
q = Sidekiq::Metrics::Query.new
|
|
82
|
+
@periods = METRICS_PERIODS
|
|
83
|
+
minutes = @periods.fetch(@period, @periods.values.first)
|
|
84
|
+
@query_result = q.for_job(@name, minutes: minutes)
|
|
85
|
+
erb(:metrics_for_job)
|
|
86
|
+
end
|
|
87
|
+
|
|
63
88
|
get "/busy" do
|
|
89
|
+
@count = (params["count"] || 100).to_i
|
|
90
|
+
(@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
|
|
91
|
+
|
|
64
92
|
erb(:busy)
|
|
65
93
|
end
|
|
66
94
|
|
|
67
95
|
post "/busy" do
|
|
68
96
|
if params["identity"]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
pro = Sidekiq::ProcessSet[params["identity"]]
|
|
98
|
+
|
|
99
|
+
pro.quiet! if params["quiet"]
|
|
100
|
+
pro.stop! if params["stop"]
|
|
72
101
|
else
|
|
73
102
|
processes.each do |pro|
|
|
103
|
+
next if pro.embedded?
|
|
104
|
+
|
|
74
105
|
pro.quiet! if params["quiet"]
|
|
75
106
|
pro.stop! if params["stop"]
|
|
76
107
|
end
|
|
@@ -294,12 +325,12 @@ module Sidekiq
|
|
|
294
325
|
end
|
|
295
326
|
|
|
296
327
|
get "/stats/queues" do
|
|
297
|
-
json Sidekiq::Stats
|
|
328
|
+
json Sidekiq::Stats.new.queues
|
|
298
329
|
end
|
|
299
330
|
|
|
300
331
|
def call(env)
|
|
301
332
|
action = self.class.match(env)
|
|
302
|
-
return [404, {"
|
|
333
|
+
return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
|
|
303
334
|
|
|
304
335
|
app = @klass
|
|
305
336
|
resp = catch(:halt) do
|
|
@@ -316,10 +347,10 @@ module Sidekiq
|
|
|
316
347
|
else
|
|
317
348
|
# rendered content goes here
|
|
318
349
|
headers = {
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
350
|
+
"content-type" => "text/html",
|
|
351
|
+
"cache-control" => "private, no-store",
|
|
352
|
+
"content-language" => action.locale,
|
|
353
|
+
"content-security-policy" => CSP_HEADER
|
|
323
354
|
}
|
|
324
355
|
# we'll let Rack calculate Content-Length for us.
|
|
325
356
|
[200, headers, [resp]]
|
|
@@ -152,7 +152,7 @@ module Sidekiq
|
|
|
152
152
|
# value and decrypt it
|
|
153
153
|
token_length = masked_token.length / 2
|
|
154
154
|
one_time_pad = masked_token[0...token_length]
|
|
155
|
-
encrypted_token = masked_token[token_length
|
|
155
|
+
encrypted_token = masked_token[token_length..]
|
|
156
156
|
xor_byte_strings(one_time_pad, encrypted_token)
|
|
157
157
|
end
|
|
158
158
|
|