sidekiq 4.2.10 → 6.1.2
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/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +2 -1
- data/.standard.yml +20 -0
- data/5.0-Upgrade.md +56 -0
- data/6.0-Upgrade.md +72 -0
- data/COMM-LICENSE +12 -10
- data/Changes.md +354 -1
- data/Ent-2.0-Upgrade.md +37 -0
- data/Ent-Changes.md +111 -3
- data/Gemfile +16 -21
- data/Gemfile.lock +192 -0
- data/LICENSE +1 -1
- data/Pro-4.0-Upgrade.md +35 -0
- data/Pro-5.0-Upgrade.md +25 -0
- data/Pro-Changes.md +181 -4
- data/README.md +19 -33
- data/Rakefile +6 -8
- data/bin/sidekiq +26 -2
- data/bin/sidekiqload +37 -34
- data/bin/sidekiqmon +8 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
- data/lib/generators/sidekiq/worker_generator.rb +21 -13
- data/lib/sidekiq.rb +86 -61
- data/lib/sidekiq/api.rb +320 -209
- data/lib/sidekiq/cli.rb +207 -217
- data/lib/sidekiq/client.rb +78 -51
- data/lib/sidekiq/delay.rb +41 -0
- data/lib/sidekiq/exception_handler.rb +12 -16
- data/lib/sidekiq/extensions/action_mailer.rb +13 -22
- data/lib/sidekiq/extensions/active_record.rb +13 -10
- data/lib/sidekiq/extensions/class_methods.rb +14 -11
- data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
- data/lib/sidekiq/fetch.rb +29 -30
- data/lib/sidekiq/job_logger.rb +63 -0
- data/lib/sidekiq/job_retry.rb +262 -0
- data/lib/sidekiq/launcher.rb +102 -69
- data/lib/sidekiq/logger.rb +165 -0
- data/lib/sidekiq/manager.rb +16 -19
- data/lib/sidekiq/middleware/chain.rb +15 -5
- data/lib/sidekiq/middleware/i18n.rb +5 -7
- data/lib/sidekiq/monitor.rb +133 -0
- data/lib/sidekiq/paginator.rb +18 -14
- data/lib/sidekiq/processor.rb +161 -82
- data/lib/sidekiq/rails.rb +27 -100
- data/lib/sidekiq/redis_connection.rb +60 -20
- data/lib/sidekiq/scheduled.rb +61 -35
- data/lib/sidekiq/sd_notify.rb +149 -0
- data/lib/sidekiq/systemd.rb +24 -0
- data/lib/sidekiq/testing.rb +48 -28
- data/lib/sidekiq/testing/inline.rb +2 -1
- data/lib/sidekiq/util.rb +20 -16
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +57 -57
- data/lib/sidekiq/web/action.rb +14 -14
- data/lib/sidekiq/web/application.rb +103 -84
- data/lib/sidekiq/web/csrf_protection.rb +158 -0
- data/lib/sidekiq/web/helpers.rb +126 -71
- data/lib/sidekiq/web/router.rb +18 -17
- data/lib/sidekiq/worker.rb +164 -41
- data/sidekiq.gemspec +15 -27
- data/web/assets/javascripts/application.js +25 -27
- data/web/assets/javascripts/dashboard.js +33 -37
- data/web/assets/stylesheets/application-dark.css +143 -0
- data/web/assets/stylesheets/application-rtl.css +246 -0
- data/web/assets/stylesheets/application.css +385 -10
- data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
- data/web/assets/stylesheets/bootstrap.css +2 -2
- data/web/locales/ar.yml +81 -0
- data/web/locales/de.yml +14 -2
- data/web/locales/en.yml +4 -0
- data/web/locales/es.yml +4 -3
- data/web/locales/fa.yml +1 -0
- data/web/locales/fr.yml +2 -2
- data/web/locales/he.yml +79 -0
- data/web/locales/ja.yml +9 -4
- data/web/locales/lt.yml +83 -0
- data/web/locales/pl.yml +4 -4
- data/web/locales/ru.yml +4 -0
- data/web/locales/ur.yml +80 -0
- data/web/locales/vi.yml +83 -0
- data/web/views/_footer.erb +5 -2
- data/web/views/_job_info.erb +2 -1
- data/web/views/_nav.erb +4 -18
- data/web/views/_paging.erb +1 -1
- data/web/views/busy.erb +15 -8
- data/web/views/dashboard.erb +1 -1
- data/web/views/dead.erb +2 -2
- data/web/views/layout.erb +12 -2
- data/web/views/morgue.erb +9 -6
- data/web/views/queue.erb +18 -8
- data/web/views/queues.erb +11 -1
- data/web/views/retries.erb +14 -7
- data/web/views/retry.erb +2 -2
- data/web/views/scheduled.erb +7 -4
- metadata +41 -188
- data/.github/issue_template.md +0 -9
- data/.travis.yml +0 -18
- data/bin/sidekiqctl +0 -99
- data/lib/sidekiq/core_ext.rb +0 -119
- data/lib/sidekiq/logging.rb +0 -106
- data/lib/sidekiq/middleware/server/active_record.rb +0 -13
- data/lib/sidekiq/middleware/server/logging.rb +0 -31
- data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
data/bin/sidekiqmon
ADDED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
<% module_namespacing do -%>
|
3
|
-
class <%= class_name %>WorkerTest <
|
3
|
+
class <%= class_name %>WorkerTest < Minitest::Test
|
4
4
|
def test_example
|
5
5
|
skip "add some examples to (or delete) #{__FILE__}"
|
6
6
|
end
|
@@ -1,22 +1,24 @@
|
|
1
|
-
require
|
1
|
+
require "rails/generators/named_base"
|
2
2
|
|
3
3
|
module Sidekiq
|
4
4
|
module Generators # :nodoc:
|
5
5
|
class WorkerGenerator < ::Rails::Generators::NamedBase # :nodoc:
|
6
|
-
desc
|
6
|
+
desc "This generator creates a Sidekiq Worker in app/workers and a corresponding test"
|
7
7
|
|
8
|
-
check_class_collision suffix:
|
8
|
+
check_class_collision suffix: "Worker"
|
9
9
|
|
10
10
|
def self.default_generator_root
|
11
11
|
File.dirname(__FILE__)
|
12
12
|
end
|
13
13
|
|
14
14
|
def create_worker_file
|
15
|
-
template
|
15
|
+
template "worker.rb.erb", File.join("app/workers", class_path, "#{file_name}_worker.rb")
|
16
16
|
end
|
17
17
|
|
18
18
|
def create_test_file
|
19
|
-
|
19
|
+
return unless test_framework
|
20
|
+
|
21
|
+
if test_framework == :rspec
|
20
22
|
create_worker_spec
|
21
23
|
else
|
22
24
|
create_worker_test
|
@@ -27,23 +29,29 @@ module Sidekiq
|
|
27
29
|
|
28
30
|
def create_worker_spec
|
29
31
|
template_file = File.join(
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
"spec/workers",
|
33
|
+
class_path,
|
34
|
+
"#{file_name}_worker_spec.rb"
|
33
35
|
)
|
34
|
-
template
|
36
|
+
template "worker_spec.rb.erb", template_file
|
35
37
|
end
|
36
38
|
|
37
39
|
def create_worker_test
|
38
40
|
template_file = File.join(
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
"test/workers",
|
42
|
+
class_path,
|
43
|
+
"#{file_name}_worker_test.rb"
|
42
44
|
)
|
43
|
-
template
|
45
|
+
template "worker_test.rb.erb", template_file
|
44
46
|
end
|
45
47
|
|
48
|
+
def file_name
|
49
|
+
@_file_name ||= super.sub(/_?worker\z/i, "")
|
50
|
+
end
|
46
51
|
|
52
|
+
def test_framework
|
53
|
+
::Rails.application.config.generators.options[:rails][:test_framework]
|
54
|
+
end
|
47
55
|
end
|
48
56
|
end
|
49
57
|
end
|
data/lib/sidekiq.rb
CHANGED
@@ -1,44 +1,46 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
|
-
require 'sidekiq/version'
|
4
|
-
fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.0.0." if RUBY_PLATFORM != 'java' && RUBY_VERSION < '2.0.0'
|
5
2
|
|
6
|
-
require
|
7
|
-
|
8
|
-
require 'sidekiq/worker'
|
9
|
-
require 'sidekiq/redis_connection'
|
3
|
+
require "sidekiq/version"
|
4
|
+
fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.5.0." if RUBY_PLATFORM != "java" && Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5.0")
|
10
5
|
|
11
|
-
require
|
6
|
+
require "sidekiq/logger"
|
7
|
+
require "sidekiq/client"
|
8
|
+
require "sidekiq/worker"
|
9
|
+
require "sidekiq/redis_connection"
|
10
|
+
require "sidekiq/delay"
|
11
|
+
|
12
|
+
require "json"
|
12
13
|
|
13
14
|
module Sidekiq
|
14
|
-
NAME =
|
15
|
-
LICENSE =
|
15
|
+
NAME = "Sidekiq"
|
16
|
+
LICENSE = "See LICENSE and the LGPL-3.0 for licensing details."
|
16
17
|
|
17
18
|
DEFAULTS = {
|
18
19
|
queues: [],
|
19
20
|
labels: [],
|
20
|
-
concurrency:
|
21
|
-
require:
|
21
|
+
concurrency: 10,
|
22
|
+
require: ".",
|
23
|
+
strict: true,
|
22
24
|
environment: nil,
|
23
|
-
timeout:
|
25
|
+
timeout: 25,
|
24
26
|
poll_interval_average: nil,
|
25
|
-
average_scheduled_poll_interval:
|
27
|
+
average_scheduled_poll_interval: 5,
|
26
28
|
error_handlers: [],
|
29
|
+
death_handlers: [],
|
27
30
|
lifecycle_events: {
|
28
31
|
startup: [],
|
29
32
|
quiet: [],
|
30
33
|
shutdown: [],
|
31
|
-
heartbeat: []
|
34
|
+
heartbeat: []
|
32
35
|
},
|
33
36
|
dead_max_jobs: 10_000,
|
34
37
|
dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
|
35
|
-
reloader: proc { |&block| block.call }
|
36
|
-
executor: proc { |&block| block.call },
|
38
|
+
reloader: proc { |&block| block.call }
|
37
39
|
}
|
38
40
|
|
39
41
|
DEFAULT_WORKER_OPTIONS = {
|
40
|
-
|
41
|
-
|
42
|
+
"retry" => true,
|
43
|
+
"queue" => "default"
|
42
44
|
}
|
43
45
|
|
44
46
|
FAKE_INFO = {
|
@@ -47,7 +49,7 @@ module Sidekiq
|
|
47
49
|
"connected_clients" => "9999",
|
48
50
|
"used_memory_human" => "9P",
|
49
51
|
"used_memory_peak_human" => "9P"
|
50
|
-
}
|
52
|
+
}
|
51
53
|
|
52
54
|
def self.❨╯°□°❩╯︵┻━┻
|
53
55
|
puts "Calm down, yo."
|
@@ -56,6 +58,7 @@ module Sidekiq
|
|
56
58
|
def self.options
|
57
59
|
@options ||= DEFAULTS.dup
|
58
60
|
end
|
61
|
+
|
59
62
|
def self.options=(opts)
|
60
63
|
@options = opts
|
61
64
|
end
|
@@ -93,10 +96,15 @@ module Sidekiq
|
|
93
96
|
retryable = true
|
94
97
|
begin
|
95
98
|
yield conn
|
96
|
-
rescue Redis::
|
97
|
-
#2550 Failover can cause the server to become a
|
98
|
-
# to disconnect and reopen the socket to get back to the
|
99
|
-
|
99
|
+
rescue Redis::BaseError => ex
|
100
|
+
# 2550 Failover can cause the server to become a replica, need
|
101
|
+
# to disconnect and reopen the socket to get back to the primary.
|
102
|
+
# 4495 Use the same logic if we have a "Not enough replicas" error from the primary
|
103
|
+
if retryable && ex.message =~ /READONLY|NOREPLICAS/
|
104
|
+
conn.disconnect!
|
105
|
+
retryable = false
|
106
|
+
retry
|
107
|
+
end
|
100
108
|
raise
|
101
109
|
end
|
102
110
|
end
|
@@ -104,19 +112,17 @@ module Sidekiq
|
|
104
112
|
|
105
113
|
def self.redis_info
|
106
114
|
redis do |conn|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
conn.info
|
114
|
-
end
|
115
|
-
rescue Redis::CommandError => ex
|
116
|
-
#2850 return fake version when INFO command has (probably) been renamed
|
117
|
-
raise unless ex.message =~ /unknown command/
|
118
|
-
FAKE_INFO
|
115
|
+
# admin commands can't go through redis-namespace starting
|
116
|
+
# in redis-namespace 2.0
|
117
|
+
if conn.respond_to?(:namespace)
|
118
|
+
conn.redis.info
|
119
|
+
else
|
120
|
+
conn.info
|
119
121
|
end
|
122
|
+
rescue Redis::CommandError => ex
|
123
|
+
# 2850 return fake version when INFO command has (probably) been renamed
|
124
|
+
raise unless /unknown command/.match?(ex.message)
|
125
|
+
FAKE_INFO
|
120
126
|
end
|
121
127
|
end
|
122
128
|
|
@@ -145,46 +151,69 @@ module Sidekiq
|
|
145
151
|
end
|
146
152
|
|
147
153
|
def self.default_server_middleware
|
148
|
-
|
149
|
-
require 'sidekiq/middleware/server/logging'
|
150
|
-
|
151
|
-
Middleware::Chain.new do |m|
|
152
|
-
m.add Middleware::Server::RetryJobs
|
153
|
-
m.add Middleware::Server::Logging
|
154
|
-
end
|
154
|
+
Middleware::Chain.new
|
155
155
|
end
|
156
156
|
|
157
157
|
def self.default_worker_options=(hash)
|
158
|
-
|
158
|
+
# stringify
|
159
|
+
@default_worker_options = default_worker_options.merge(hash.transform_keys(&:to_s))
|
159
160
|
end
|
161
|
+
|
160
162
|
def self.default_worker_options
|
161
163
|
defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
|
162
164
|
end
|
163
165
|
|
166
|
+
##
|
167
|
+
# Death handlers are called when all retries for a job have been exhausted and
|
168
|
+
# the job dies. It's the notification to your application
|
169
|
+
# that this job will not succeed without manual intervention.
|
170
|
+
#
|
164
171
|
# Sidekiq.configure_server do |config|
|
165
|
-
# config.
|
172
|
+
# config.death_handlers << ->(job, ex) do
|
166
173
|
# end
|
167
174
|
# end
|
168
|
-
def self.
|
169
|
-
|
170
|
-
end
|
171
|
-
@default_retries_exhausted = ->(job, ex) { }
|
172
|
-
def self.default_retries_exhausted
|
173
|
-
@default_retries_exhausted
|
175
|
+
def self.death_handlers
|
176
|
+
options[:death_handlers]
|
174
177
|
end
|
175
178
|
|
176
179
|
def self.load_json(string)
|
177
180
|
JSON.parse(string)
|
178
181
|
end
|
182
|
+
|
179
183
|
def self.dump_json(object)
|
180
184
|
JSON.generate(object)
|
181
185
|
end
|
182
186
|
|
187
|
+
def self.log_formatter
|
188
|
+
@log_formatter ||= if ENV["DYNO"]
|
189
|
+
Sidekiq::Logger::Formatters::WithoutTimestamp.new
|
190
|
+
else
|
191
|
+
Sidekiq::Logger::Formatters::Pretty.new
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.log_formatter=(log_formatter)
|
196
|
+
@log_formatter = log_formatter
|
197
|
+
logger.formatter = log_formatter
|
198
|
+
end
|
199
|
+
|
183
200
|
def self.logger
|
184
|
-
Sidekiq::
|
201
|
+
@logger ||= Sidekiq::Logger.new($stdout, level: Logger::INFO)
|
185
202
|
end
|
186
|
-
|
187
|
-
|
203
|
+
|
204
|
+
def self.logger=(logger)
|
205
|
+
if logger.nil?
|
206
|
+
self.logger.level = Logger::FATAL
|
207
|
+
return self.logger
|
208
|
+
end
|
209
|
+
|
210
|
+
logger.extend(Sidekiq::LoggingUtils)
|
211
|
+
|
212
|
+
@logger = logger
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.pro?
|
216
|
+
defined?(Sidekiq::Pro)
|
188
217
|
end
|
189
218
|
|
190
219
|
# How frequently Redis should be checked by a random Sidekiq process for
|
@@ -193,7 +222,7 @@ module Sidekiq
|
|
193
222
|
#
|
194
223
|
# See sidekiq/scheduled.rb for an in-depth explanation of this value
|
195
224
|
def self.average_scheduled_poll_interval=(interval)
|
196
|
-
|
225
|
+
options[:average_scheduled_poll_interval] = interval
|
197
226
|
end
|
198
227
|
|
199
228
|
# Register a proc to handle any error which occurs within the Sidekiq process.
|
@@ -204,7 +233,7 @@ module Sidekiq
|
|
204
233
|
#
|
205
234
|
# The default error handler logs errors to Sidekiq.logger.
|
206
235
|
def self.error_handlers
|
207
|
-
|
236
|
+
options[:error_handlers]
|
208
237
|
end
|
209
238
|
|
210
239
|
# Register a block to run at a point in the Sidekiq lifecycle.
|
@@ -228,10 +257,6 @@ module Sidekiq
|
|
228
257
|
# otherwise Ruby's Thread#kill will commit. See #377.
|
229
258
|
# DO NOT RESCUE THIS ERROR IN YOUR WORKERS
|
230
259
|
class Shutdown < Interrupt; end
|
231
|
-
|
232
260
|
end
|
233
261
|
|
234
|
-
require
|
235
|
-
require 'sidekiq/extensions/action_mailer'
|
236
|
-
require 'sidekiq/extensions/active_record'
|
237
|
-
require 'sidekiq/rails' if defined?(::Rails::Engine)
|
262
|
+
require "sidekiq/rails" if defined?(::Rails::Engine)
|
data/lib/sidekiq/api.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
|
-
|
2
|
+
|
3
|
+
require "sidekiq"
|
4
|
+
|
5
|
+
require "zlib"
|
6
|
+
require "base64"
|
4
7
|
|
5
8
|
module Sidekiq
|
6
9
|
class Stats
|
@@ -49,55 +52,65 @@ module Sidekiq
|
|
49
52
|
end
|
50
53
|
|
51
54
|
def fetch_stats!
|
52
|
-
pipe1_res = Sidekiq.redis
|
55
|
+
pipe1_res = Sidekiq.redis { |conn|
|
53
56
|
conn.pipelined do
|
54
|
-
conn.get(
|
55
|
-
conn.get(
|
56
|
-
conn.zcard(
|
57
|
-
conn.zcard(
|
58
|
-
conn.zcard(
|
59
|
-
conn.scard(
|
60
|
-
conn.lrange(
|
61
|
-
conn.smembers('processes'.freeze)
|
62
|
-
conn.smembers('queues'.freeze)
|
57
|
+
conn.get("stat:processed")
|
58
|
+
conn.get("stat:failed")
|
59
|
+
conn.zcard("schedule")
|
60
|
+
conn.zcard("retry")
|
61
|
+
conn.zcard("dead")
|
62
|
+
conn.scard("processes")
|
63
|
+
conn.lrange("queue:default", -1, -1)
|
63
64
|
end
|
64
|
-
|
65
|
+
}
|
65
66
|
|
66
|
-
|
67
|
+
processes = Sidekiq.redis { |conn|
|
68
|
+
conn.sscan_each("processes").to_a
|
69
|
+
}
|
70
|
+
|
71
|
+
queues = Sidekiq.redis { |conn|
|
72
|
+
conn.sscan_each("queues").to_a
|
73
|
+
}
|
74
|
+
|
75
|
+
pipe2_res = Sidekiq.redis { |conn|
|
67
76
|
conn.pipelined do
|
68
|
-
|
69
|
-
|
77
|
+
processes.each { |key| conn.hget(key, "busy") }
|
78
|
+
queues.each { |queue| conn.llen("queue:#{queue}") }
|
70
79
|
end
|
71
|
-
|
80
|
+
}
|
72
81
|
|
73
|
-
s =
|
74
|
-
workers_size = pipe2_res[0...s].
|
75
|
-
enqueued
|
82
|
+
s = processes.size
|
83
|
+
workers_size = pipe2_res[0...s].sum(&:to_i)
|
84
|
+
enqueued = pipe2_res[s..-1].sum(&:to_i)
|
76
85
|
|
77
86
|
default_queue_latency = if (entry = pipe1_res[6].first)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
87
|
+
job = begin
|
88
|
+
Sidekiq.load_json(entry)
|
89
|
+
rescue
|
90
|
+
{}
|
91
|
+
end
|
92
|
+
now = Time.now.to_f
|
93
|
+
thence = job["enqueued_at"] || now
|
94
|
+
now - thence
|
95
|
+
else
|
96
|
+
0
|
97
|
+
end
|
85
98
|
@stats = {
|
86
|
-
processed:
|
87
|
-
failed:
|
88
|
-
scheduled_size:
|
89
|
-
retry_size:
|
90
|
-
dead_size:
|
91
|
-
processes_size:
|
99
|
+
processed: pipe1_res[0].to_i,
|
100
|
+
failed: pipe1_res[1].to_i,
|
101
|
+
scheduled_size: pipe1_res[2],
|
102
|
+
retry_size: pipe1_res[3],
|
103
|
+
dead_size: pipe1_res[4],
|
104
|
+
processes_size: pipe1_res[5],
|
92
105
|
|
93
106
|
default_queue_latency: default_queue_latency,
|
94
|
-
workers_size:
|
95
|
-
enqueued:
|
107
|
+
workers_size: workers_size,
|
108
|
+
enqueued: enqueued
|
96
109
|
}
|
97
110
|
end
|
98
111
|
|
99
112
|
def reset(*stats)
|
100
|
-
all
|
113
|
+
all = %w[failed processed]
|
101
114
|
stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
|
102
115
|
|
103
116
|
mset_args = []
|
@@ -119,22 +132,16 @@ module Sidekiq
|
|
119
132
|
class Queues
|
120
133
|
def lengths
|
121
134
|
Sidekiq.redis do |conn|
|
122
|
-
queues = conn.
|
135
|
+
queues = conn.sscan_each("queues").to_a
|
123
136
|
|
124
|
-
lengths = conn.pipelined
|
137
|
+
lengths = conn.pipelined {
|
125
138
|
queues.each do |queue|
|
126
139
|
conn.llen("queue:#{queue}")
|
127
140
|
end
|
128
|
-
|
129
|
-
|
130
|
-
i = 0
|
131
|
-
array_of_arrays = queues.inject({}) do |memo, queue|
|
132
|
-
memo[queue] = lengths[i]
|
133
|
-
i += 1
|
134
|
-
memo
|
135
|
-
end.sort_by { |_, size| size }
|
141
|
+
}
|
136
142
|
|
137
|
-
|
143
|
+
array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
|
144
|
+
Hash[array_of_arrays]
|
138
145
|
end
|
139
146
|
end
|
140
147
|
end
|
@@ -146,33 +153,32 @@ module Sidekiq
|
|
146
153
|
end
|
147
154
|
|
148
155
|
def processed
|
149
|
-
date_stat_hash("processed")
|
156
|
+
@processed ||= date_stat_hash("processed")
|
150
157
|
end
|
151
158
|
|
152
159
|
def failed
|
153
|
-
date_stat_hash("failed")
|
160
|
+
@failed ||= date_stat_hash("failed")
|
154
161
|
end
|
155
162
|
|
156
163
|
private
|
157
164
|
|
158
165
|
def date_stat_hash(stat)
|
159
|
-
i = 0
|
160
166
|
stat_hash = {}
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
while i < @days_previous
|
165
|
-
date = @start_date - i
|
166
|
-
datestr = date.strftime("%Y-%m-%d".freeze)
|
167
|
-
keys << "stat:#{stat}:#{datestr}"
|
168
|
-
dates << datestr
|
169
|
-
i += 1
|
170
|
-
end
|
167
|
+
dates = @start_date.downto(@start_date - @days_previous + 1).map { |date|
|
168
|
+
date.strftime("%Y-%m-%d")
|
169
|
+
}
|
171
170
|
|
172
|
-
|
173
|
-
|
174
|
-
|
171
|
+
keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
|
172
|
+
|
173
|
+
begin
|
174
|
+
Sidekiq.redis do |conn|
|
175
|
+
conn.mget(keys).each_with_index do |value, idx|
|
176
|
+
stat_hash[dates[idx]] = value ? value.to_i : 0
|
177
|
+
end
|
175
178
|
end
|
179
|
+
rescue Redis::CommandError
|
180
|
+
# mget will trigger a CROSSSLOT error when run against a Cluster
|
181
|
+
# TODO Someone want to add Cluster support?
|
176
182
|
end
|
177
183
|
|
178
184
|
stat_hash
|
@@ -199,13 +205,13 @@ module Sidekiq
|
|
199
205
|
# Return all known queues within Redis.
|
200
206
|
#
|
201
207
|
def self.all
|
202
|
-
Sidekiq.redis { |c| c.
|
208
|
+
Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
|
203
209
|
end
|
204
210
|
|
205
211
|
attr_reader :name
|
206
212
|
|
207
|
-
def initialize(name="default")
|
208
|
-
@name = name
|
213
|
+
def initialize(name = "default")
|
214
|
+
@name = name.to_s
|
209
215
|
@rname = "queue:#{name}"
|
210
216
|
end
|
211
217
|
|
@@ -224,13 +230,13 @@ module Sidekiq
|
|
224
230
|
#
|
225
231
|
# @return Float
|
226
232
|
def latency
|
227
|
-
entry = Sidekiq.redis
|
233
|
+
entry = Sidekiq.redis { |conn|
|
228
234
|
conn.lrange(@rname, -1, -1)
|
229
|
-
|
235
|
+
}.first
|
230
236
|
return 0 unless entry
|
231
237
|
job = Sidekiq.load_json(entry)
|
232
238
|
now = Time.now.to_f
|
233
|
-
thence = job[
|
239
|
+
thence = job["enqueued_at"] || now
|
234
240
|
now - thence
|
235
241
|
end
|
236
242
|
|
@@ -240,12 +246,12 @@ module Sidekiq
|
|
240
246
|
page = 0
|
241
247
|
page_size = 50
|
242
248
|
|
243
|
-
|
249
|
+
loop do
|
244
250
|
range_start = page * page_size - deleted_size
|
245
|
-
range_end
|
246
|
-
entries = Sidekiq.redis
|
251
|
+
range_end = range_start + page_size - 1
|
252
|
+
entries = Sidekiq.redis { |conn|
|
247
253
|
conn.lrange @rname, range_start, range_end
|
248
|
-
|
254
|
+
}
|
249
255
|
break if entries.empty?
|
250
256
|
page += 1
|
251
257
|
entries.each do |entry|
|
@@ -267,8 +273,8 @@ module Sidekiq
|
|
267
273
|
def clear
|
268
274
|
Sidekiq.redis do |conn|
|
269
275
|
conn.multi do
|
270
|
-
conn.
|
271
|
-
conn.srem("queues"
|
276
|
+
conn.unlink(@rname)
|
277
|
+
conn.srem("queues", name)
|
272
278
|
end
|
273
279
|
end
|
274
280
|
end
|
@@ -286,14 +292,26 @@ module Sidekiq
|
|
286
292
|
attr_reader :item
|
287
293
|
attr_reader :value
|
288
294
|
|
289
|
-
def initialize(item, queue_name=nil)
|
295
|
+
def initialize(item, queue_name = nil)
|
296
|
+
@args = nil
|
290
297
|
@value = item
|
291
|
-
@item = item.is_a?(Hash) ? item :
|
292
|
-
@queue = queue_name || @item[
|
298
|
+
@item = item.is_a?(Hash) ? item : parse(item)
|
299
|
+
@queue = queue_name || @item["queue"]
|
300
|
+
end
|
301
|
+
|
302
|
+
def parse(item)
|
303
|
+
Sidekiq.load_json(item)
|
304
|
+
rescue JSON::ParserError
|
305
|
+
# If the job payload in Redis is invalid JSON, we'll load
|
306
|
+
# the item as an empty hash and store the invalid JSON as
|
307
|
+
# the job 'args' for display in the Web UI.
|
308
|
+
@invalid = true
|
309
|
+
@args = [item]
|
310
|
+
{}
|
293
311
|
end
|
294
312
|
|
295
313
|
def klass
|
296
|
-
|
314
|
+
self["class"]
|
297
315
|
end
|
298
316
|
|
299
317
|
def display_class
|
@@ -304,86 +322,123 @@ module Sidekiq
|
|
304
322
|
"#{target}.#{method}"
|
305
323
|
end
|
306
324
|
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
307
|
-
job_class = @item[
|
308
|
-
if
|
325
|
+
job_class = @item["wrapped"] || args[0]
|
326
|
+
if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
|
309
327
|
# MailerClass#mailer_method
|
310
|
-
args[0][
|
328
|
+
args[0]["arguments"][0..1].join("#")
|
311
329
|
else
|
312
|
-
|
330
|
+
job_class
|
313
331
|
end
|
314
332
|
else
|
315
333
|
klass
|
316
|
-
|
334
|
+
end
|
317
335
|
end
|
318
336
|
|
319
337
|
def display_args
|
320
338
|
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
321
|
-
@
|
339
|
+
@display_args ||= case klass
|
322
340
|
when /\ASidekiq::Extensions::Delayed/
|
323
341
|
safe_load(args[0], args) do |_, _, arg|
|
324
342
|
arg
|
325
343
|
end
|
326
344
|
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
327
|
-
job_args =
|
328
|
-
if
|
329
|
-
|
330
|
-
|
345
|
+
job_args = self["wrapped"] ? args[0]["arguments"] : []
|
346
|
+
if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
|
347
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
348
|
+
job_args.drop(3)
|
349
|
+
elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
|
350
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
351
|
+
job_args.drop(3).first["args"]
|
331
352
|
else
|
332
|
-
|
353
|
+
job_args
|
333
354
|
end
|
334
355
|
else
|
356
|
+
if self["encrypt"]
|
357
|
+
# no point in showing 150+ bytes of random garbage
|
358
|
+
args[-1] = "[encrypted data]"
|
359
|
+
end
|
335
360
|
args
|
336
|
-
|
361
|
+
end
|
337
362
|
end
|
338
363
|
|
339
364
|
def args
|
340
|
-
@item[
|
365
|
+
@args || @item["args"]
|
341
366
|
end
|
342
367
|
|
343
368
|
def jid
|
344
|
-
|
369
|
+
self["jid"]
|
345
370
|
end
|
346
371
|
|
347
372
|
def enqueued_at
|
348
|
-
|
373
|
+
self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
|
349
374
|
end
|
350
375
|
|
351
376
|
def created_at
|
352
|
-
Time.at(
|
377
|
+
Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
|
353
378
|
end
|
354
379
|
|
355
|
-
def
|
356
|
-
|
380
|
+
def tags
|
381
|
+
self["tags"] || []
|
357
382
|
end
|
358
383
|
|
384
|
+
def error_backtrace
|
385
|
+
# Cache nil values
|
386
|
+
if defined?(@error_backtrace)
|
387
|
+
@error_backtrace
|
388
|
+
else
|
389
|
+
value = self["error_backtrace"]
|
390
|
+
@error_backtrace = value && uncompress_backtrace(value)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
attr_reader :queue
|
395
|
+
|
359
396
|
def latency
|
360
397
|
now = Time.now.to_f
|
361
|
-
now - (@item[
|
398
|
+
now - (@item["enqueued_at"] || @item["created_at"] || now)
|
362
399
|
end
|
363
400
|
|
364
401
|
##
|
365
402
|
# Remove this job from the queue.
|
366
403
|
def delete
|
367
|
-
count = Sidekiq.redis
|
404
|
+
count = Sidekiq.redis { |conn|
|
368
405
|
conn.lrem("queue:#{@queue}", 1, @value)
|
369
|
-
|
406
|
+
}
|
370
407
|
count != 0
|
371
408
|
end
|
372
409
|
|
373
410
|
def [](name)
|
374
|
-
|
411
|
+
# nil will happen if the JSON fails to parse.
|
412
|
+
# We don't guarantee Sidekiq will work with bad job JSON but we should
|
413
|
+
# make a best effort to minimize the damage.
|
414
|
+
@item ? @item[name] : nil
|
375
415
|
end
|
376
416
|
|
377
417
|
private
|
378
418
|
|
379
419
|
def safe_load(content, default)
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
420
|
+
yield(*YAML.load(content))
|
421
|
+
rescue => ex
|
422
|
+
# #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
|
423
|
+
# memory yet so the YAML can't be loaded.
|
424
|
+
Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
|
425
|
+
default
|
426
|
+
end
|
427
|
+
|
428
|
+
def uncompress_backtrace(backtrace)
|
429
|
+
if backtrace.is_a?(Array)
|
430
|
+
# Handle old jobs with raw Array backtrace format
|
431
|
+
backtrace
|
432
|
+
else
|
433
|
+
decoded = Base64.decode64(backtrace)
|
434
|
+
uncompressed = Zlib::Inflate.inflate(decoded)
|
435
|
+
begin
|
436
|
+
Sidekiq.load_json(uncompressed)
|
437
|
+
rescue
|
438
|
+
# Handle old jobs with marshalled backtrace format
|
439
|
+
# TODO Remove in 7.x
|
440
|
+
Marshal.load(uncompressed)
|
441
|
+
end
|
387
442
|
end
|
388
443
|
end
|
389
444
|
end
|
@@ -411,8 +466,9 @@ module Sidekiq
|
|
411
466
|
end
|
412
467
|
|
413
468
|
def reschedule(at)
|
414
|
-
|
415
|
-
|
469
|
+
Sidekiq.redis do |conn|
|
470
|
+
conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
|
471
|
+
end
|
416
472
|
end
|
417
473
|
|
418
474
|
def add_to_queue
|
@@ -425,7 +481,7 @@ module Sidekiq
|
|
425
481
|
def retry
|
426
482
|
remove_job do |message|
|
427
483
|
msg = Sidekiq.load_json(message)
|
428
|
-
msg[
|
484
|
+
msg["retry_count"] -= 1 if msg["retry_count"]
|
429
485
|
Sidekiq::Client.push(msg)
|
430
486
|
end
|
431
487
|
end
|
@@ -434,57 +490,49 @@ module Sidekiq
|
|
434
490
|
# Place job in the dead set
|
435
491
|
def kill
|
436
492
|
remove_job do |message|
|
437
|
-
|
438
|
-
Sidekiq.redis do |conn|
|
439
|
-
conn.multi do
|
440
|
-
conn.zadd('dead', now, message)
|
441
|
-
conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
|
442
|
-
conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
|
443
|
-
end
|
444
|
-
end
|
493
|
+
DeadSet.new.kill(message)
|
445
494
|
end
|
446
495
|
end
|
447
496
|
|
448
497
|
def error?
|
449
|
-
!!item[
|
498
|
+
!!item["error_class"]
|
450
499
|
end
|
451
500
|
|
452
501
|
private
|
453
502
|
|
454
503
|
def remove_job
|
455
504
|
Sidekiq.redis do |conn|
|
456
|
-
results = conn.multi
|
505
|
+
results = conn.multi {
|
457
506
|
conn.zrangebyscore(parent.name, score, score)
|
458
507
|
conn.zremrangebyscore(parent.name, score, score)
|
459
|
-
|
508
|
+
}.first
|
460
509
|
|
461
510
|
if results.size == 1
|
462
511
|
yield results.first
|
463
512
|
else
|
464
513
|
# multiple jobs with the same score
|
465
514
|
# find the one with the right JID and push it
|
466
|
-
|
515
|
+
matched, nonmatched = results.partition { |message|
|
467
516
|
if message.index(jid)
|
468
517
|
msg = Sidekiq.load_json(message)
|
469
|
-
msg[
|
518
|
+
msg["jid"] == jid
|
470
519
|
else
|
471
520
|
false
|
472
521
|
end
|
473
|
-
|
522
|
+
}
|
474
523
|
|
475
|
-
msg =
|
524
|
+
msg = matched.first
|
476
525
|
yield msg if msg
|
477
526
|
|
478
527
|
# push the rest back onto the sorted set
|
479
528
|
conn.multi do
|
480
|
-
|
529
|
+
nonmatched.each do |message|
|
481
530
|
conn.zadd(parent.name, score.to_f.to_s, message)
|
482
531
|
end
|
483
532
|
end
|
484
533
|
end
|
485
534
|
end
|
486
535
|
end
|
487
|
-
|
488
536
|
end
|
489
537
|
|
490
538
|
class SortedSet
|
@@ -501,16 +549,26 @@ module Sidekiq
|
|
501
549
|
Sidekiq.redis { |c| c.zcard(name) }
|
502
550
|
end
|
503
551
|
|
552
|
+
def scan(match, count = 100)
|
553
|
+
return to_enum(:scan, match, count) unless block_given?
|
554
|
+
|
555
|
+
match = "*#{match}*" unless match.include?("*")
|
556
|
+
Sidekiq.redis do |conn|
|
557
|
+
conn.zscan_each(name, match: match, count: count) do |entry, score|
|
558
|
+
yield SortedEntry.new(self, score, entry)
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
504
563
|
def clear
|
505
564
|
Sidekiq.redis do |conn|
|
506
|
-
conn.
|
565
|
+
conn.unlink(name)
|
507
566
|
end
|
508
567
|
end
|
509
568
|
alias_method :💣, :clear
|
510
569
|
end
|
511
570
|
|
512
571
|
class JobSet < SortedSet
|
513
|
-
|
514
572
|
def schedule(timestamp, message)
|
515
573
|
Sidekiq.redis do |conn|
|
516
574
|
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
|
@@ -523,44 +581,55 @@ module Sidekiq
|
|
523
581
|
page = -1
|
524
582
|
page_size = 50
|
525
583
|
|
526
|
-
|
584
|
+
loop do
|
527
585
|
range_start = page * page_size + offset_size
|
528
|
-
range_end
|
529
|
-
elements = Sidekiq.redis
|
586
|
+
range_end = range_start + page_size - 1
|
587
|
+
elements = Sidekiq.redis { |conn|
|
530
588
|
conn.zrange name, range_start, range_end, with_scores: true
|
531
|
-
|
589
|
+
}
|
532
590
|
break if elements.empty?
|
533
591
|
page -= 1
|
534
|
-
elements.
|
592
|
+
elements.reverse_each do |element, score|
|
535
593
|
yield SortedEntry.new(self, score, element)
|
536
594
|
end
|
537
595
|
offset_size = initial_size - @_size
|
538
596
|
end
|
539
597
|
end
|
540
598
|
|
599
|
+
##
|
600
|
+
# Fetch jobs that match a given time or Range. Job ID is an
|
601
|
+
# optional second argument.
|
541
602
|
def fetch(score, jid = nil)
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
elements.inject([]) do |result, element|
|
547
|
-
entry = SortedEntry.new(self, score, element)
|
548
|
-
if jid
|
549
|
-
result << entry if entry.jid == jid
|
603
|
+
begin_score, end_score =
|
604
|
+
if score.is_a?(Range)
|
605
|
+
[score.first, score.last]
|
550
606
|
else
|
551
|
-
|
607
|
+
[score, score]
|
552
608
|
end
|
553
|
-
|
609
|
+
|
610
|
+
elements = Sidekiq.redis { |conn|
|
611
|
+
conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
|
612
|
+
}
|
613
|
+
|
614
|
+
elements.each_with_object([]) do |element, result|
|
615
|
+
data, job_score = element
|
616
|
+
entry = SortedEntry.new(self, job_score, data)
|
617
|
+
result << entry if jid.nil? || entry.jid == jid
|
554
618
|
end
|
555
619
|
end
|
556
620
|
|
557
621
|
##
|
558
622
|
# Find the job with the given JID within this sorted set.
|
559
|
-
#
|
560
|
-
# This is a slow, inefficient operation. Do not use under
|
561
|
-
# normal conditions. Sidekiq Pro contains a faster version.
|
623
|
+
# This is a slower O(n) operation. Do not use for app logic.
|
562
624
|
def find_job(jid)
|
563
|
-
|
625
|
+
Sidekiq.redis do |conn|
|
626
|
+
conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
|
627
|
+
job = JSON.parse(entry)
|
628
|
+
matched = job["jid"] == jid
|
629
|
+
return SortedEntry.new(self, score, entry) if matched
|
630
|
+
end
|
631
|
+
end
|
632
|
+
nil
|
564
633
|
end
|
565
634
|
|
566
635
|
def delete_by_value(name, value)
|
@@ -575,13 +644,14 @@ module Sidekiq
|
|
575
644
|
Sidekiq.redis do |conn|
|
576
645
|
elements = conn.zrangebyscore(name, score, score)
|
577
646
|
elements.each do |element|
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
647
|
+
if element.index(jid)
|
648
|
+
message = Sidekiq.load_json(element)
|
649
|
+
if message["jid"] == jid
|
650
|
+
ret = conn.zrem(name, element)
|
651
|
+
@_size -= 1 if ret
|
652
|
+
break ret
|
653
|
+
end
|
583
654
|
end
|
584
|
-
false
|
585
655
|
end
|
586
656
|
end
|
587
657
|
end
|
@@ -603,7 +673,7 @@ module Sidekiq
|
|
603
673
|
# end.map(&:delete)
|
604
674
|
class ScheduledSet < JobSet
|
605
675
|
def initialize
|
606
|
-
super
|
676
|
+
super "schedule"
|
607
677
|
end
|
608
678
|
end
|
609
679
|
|
@@ -621,13 +691,15 @@ module Sidekiq
|
|
621
691
|
# end.map(&:delete)
|
622
692
|
class RetrySet < JobSet
|
623
693
|
def initialize
|
624
|
-
super
|
694
|
+
super "retry"
|
625
695
|
end
|
626
696
|
|
627
697
|
def retry_all
|
628
|
-
while size > 0
|
629
|
-
|
630
|
-
|
698
|
+
each(&:retry) while size > 0
|
699
|
+
end
|
700
|
+
|
701
|
+
def kill_all
|
702
|
+
each(&:kill) while size > 0
|
631
703
|
end
|
632
704
|
end
|
633
705
|
|
@@ -636,13 +708,32 @@ module Sidekiq
|
|
636
708
|
#
|
637
709
|
class DeadSet < JobSet
|
638
710
|
def initialize
|
639
|
-
super
|
711
|
+
super "dead"
|
640
712
|
end
|
641
713
|
|
642
|
-
def
|
643
|
-
|
644
|
-
|
714
|
+
def kill(message, opts = {})
|
715
|
+
now = Time.now.to_f
|
716
|
+
Sidekiq.redis do |conn|
|
717
|
+
conn.multi do
|
718
|
+
conn.zadd(name, now.to_s, message)
|
719
|
+
conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
|
720
|
+
conn.zremrangebyrank(name, 0, - self.class.max_jobs)
|
721
|
+
end
|
722
|
+
end
|
723
|
+
|
724
|
+
if opts[:notify_failure] != false
|
725
|
+
job = Sidekiq.load_json(message)
|
726
|
+
r = RuntimeError.new("Job killed by API")
|
727
|
+
r.set_backtrace(caller)
|
728
|
+
Sidekiq.death_handlers.each do |handle|
|
729
|
+
handle.call(job, r)
|
730
|
+
end
|
645
731
|
end
|
732
|
+
true
|
733
|
+
end
|
734
|
+
|
735
|
+
def retry_all
|
736
|
+
each(&:retry) while size > 0
|
646
737
|
end
|
647
738
|
|
648
739
|
def self.max_jobs
|
@@ -656,7 +747,7 @@ module Sidekiq
|
|
656
747
|
|
657
748
|
##
|
658
749
|
# Enumerates the set of Sidekiq processes which are actively working
|
659
|
-
# right now. Each process
|
750
|
+
# right now. Each process sends a heartbeat to Redis every 5 seconds
|
660
751
|
# so this set should be relatively accurate, barring network partitions.
|
661
752
|
#
|
662
753
|
# Yields a Sidekiq::Process.
|
@@ -664,54 +755,56 @@ module Sidekiq
|
|
664
755
|
class ProcessSet
|
665
756
|
include Enumerable
|
666
757
|
|
667
|
-
def initialize(clean_plz=true)
|
668
|
-
|
758
|
+
def initialize(clean_plz = true)
|
759
|
+
cleanup if clean_plz
|
669
760
|
end
|
670
761
|
|
671
762
|
# Cleans up dead processes recorded in Redis.
|
672
763
|
# Returns the number of processes cleaned.
|
673
|
-
def
|
764
|
+
def cleanup
|
674
765
|
count = 0
|
675
766
|
Sidekiq.redis do |conn|
|
676
|
-
procs = conn.
|
677
|
-
heartbeats = conn.pipelined
|
767
|
+
procs = conn.sscan_each("processes").to_a.sort
|
768
|
+
heartbeats = conn.pipelined {
|
678
769
|
procs.each do |key|
|
679
|
-
conn.hget(key,
|
770
|
+
conn.hget(key, "info")
|
680
771
|
end
|
681
|
-
|
772
|
+
}
|
682
773
|
|
683
774
|
# the hash named key has an expiry of 60 seconds.
|
684
775
|
# if it's not found, that means the process has not reported
|
685
776
|
# in to Redis and probably died.
|
686
|
-
to_prune =
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
count = conn.srem('processes', to_prune) unless to_prune.empty?
|
777
|
+
to_prune = procs.select.with_index { |proc, i|
|
778
|
+
heartbeats[i].nil?
|
779
|
+
}
|
780
|
+
count = conn.srem("processes", to_prune) unless to_prune.empty?
|
691
781
|
end
|
692
782
|
count
|
693
783
|
end
|
694
784
|
|
695
785
|
def each
|
696
|
-
|
786
|
+
result = Sidekiq.redis { |conn|
|
787
|
+
procs = conn.sscan_each("processes").to_a.sort
|
697
788
|
|
698
|
-
Sidekiq.redis do |conn|
|
699
789
|
# We're making a tradeoff here between consuming more memory instead of
|
700
790
|
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
701
791
|
# you'll be happier this way
|
702
|
-
|
792
|
+
conn.pipelined do
|
703
793
|
procs.each do |key|
|
704
|
-
conn.hmget(key,
|
794
|
+
conn.hmget(key, "info", "busy", "beat", "quiet")
|
705
795
|
end
|
706
796
|
end
|
797
|
+
}
|
707
798
|
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
799
|
+
result.each do |info, busy, at_s, quiet|
|
800
|
+
# If a process is stopped between when we query Redis for `procs` and
|
801
|
+
# when we query for `result`, we will have an item in `result` that is
|
802
|
+
# composed of `nil` values.
|
803
|
+
next if info.nil?
|
713
804
|
|
714
|
-
|
805
|
+
hash = Sidekiq.load_json(info)
|
806
|
+
yield Process.new(hash.merge("busy" => busy.to_i, "beat" => at_s.to_f, "quiet" => quiet))
|
807
|
+
end
|
715
808
|
end
|
716
809
|
|
717
810
|
# This method is not guaranteed accurate since it does not prune the set
|
@@ -719,7 +812,19 @@ module Sidekiq
|
|
719
812
|
# contains Sidekiq processes which have sent a heartbeat within the last
|
720
813
|
# 60 seconds.
|
721
814
|
def size
|
722
|
-
Sidekiq.redis { |conn| conn.scard(
|
815
|
+
Sidekiq.redis { |conn| conn.scard("processes") }
|
816
|
+
end
|
817
|
+
|
818
|
+
# Returns the identity of the current cluster leader or "" if no leader.
|
819
|
+
# This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
|
820
|
+
# or Sidekiq Pro.
|
821
|
+
def leader
|
822
|
+
@leader ||= begin
|
823
|
+
x = Sidekiq.redis { |c| c.get("dear-leader") }
|
824
|
+
# need a non-falsy value so we can memoize
|
825
|
+
x ||= ""
|
826
|
+
x
|
827
|
+
end
|
723
828
|
end
|
724
829
|
end
|
725
830
|
|
@@ -744,31 +849,35 @@ module Sidekiq
|
|
744
849
|
end
|
745
850
|
|
746
851
|
def tag
|
747
|
-
self[
|
852
|
+
self["tag"]
|
748
853
|
end
|
749
854
|
|
750
855
|
def labels
|
751
|
-
Array(self[
|
856
|
+
Array(self["labels"])
|
752
857
|
end
|
753
858
|
|
754
859
|
def [](key)
|
755
860
|
@attribs[key]
|
756
861
|
end
|
757
862
|
|
863
|
+
def identity
|
864
|
+
self["identity"]
|
865
|
+
end
|
866
|
+
|
758
867
|
def quiet!
|
759
|
-
signal(
|
868
|
+
signal("TSTP")
|
760
869
|
end
|
761
870
|
|
762
871
|
def stop!
|
763
|
-
signal(
|
872
|
+
signal("TERM")
|
764
873
|
end
|
765
874
|
|
766
875
|
def dump_threads
|
767
|
-
signal(
|
876
|
+
signal("TTIN")
|
768
877
|
end
|
769
878
|
|
770
879
|
def stopping?
|
771
|
-
self[
|
880
|
+
self["quiet"] == "true"
|
772
881
|
end
|
773
882
|
|
774
883
|
private
|
@@ -782,10 +891,6 @@ module Sidekiq
|
|
782
891
|
end
|
783
892
|
end
|
784
893
|
end
|
785
|
-
|
786
|
-
def identity
|
787
|
-
self['identity']
|
788
|
-
end
|
789
894
|
end
|
790
895
|
|
791
896
|
##
|
@@ -811,20 +916,27 @@ module Sidekiq
|
|
811
916
|
class Workers
|
812
917
|
include Enumerable
|
813
918
|
|
814
|
-
def each
|
919
|
+
def each(&block)
|
920
|
+
results = []
|
815
921
|
Sidekiq.redis do |conn|
|
816
|
-
procs = conn.
|
922
|
+
procs = conn.sscan_each("processes").to_a
|
817
923
|
procs.sort.each do |key|
|
818
|
-
valid, workers = conn.pipelined
|
819
|
-
conn.exists(key)
|
924
|
+
valid, workers = conn.pipelined {
|
925
|
+
conn.exists?(key)
|
820
926
|
conn.hgetall("#{key}:workers")
|
821
|
-
|
927
|
+
}
|
822
928
|
next unless valid
|
823
929
|
workers.each_pair do |tid, json|
|
824
|
-
|
930
|
+
hsh = Sidekiq.load_json(json)
|
931
|
+
p = hsh["payload"]
|
932
|
+
# avoid breaking API, this is a side effect of the JSON optimization in #4316
|
933
|
+
hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
|
934
|
+
results << [key, tid, hsh]
|
825
935
|
end
|
826
936
|
end
|
827
937
|
end
|
938
|
+
|
939
|
+
results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
|
828
940
|
end
|
829
941
|
|
830
942
|
# Note that #size is only as accurate as Sidekiq's heartbeat,
|
@@ -835,18 +947,17 @@ module Sidekiq
|
|
835
947
|
# which can easily get out of sync with crashy processes.
|
836
948
|
def size
|
837
949
|
Sidekiq.redis do |conn|
|
838
|
-
procs = conn.
|
950
|
+
procs = conn.sscan_each("processes").to_a
|
839
951
|
if procs.empty?
|
840
952
|
0
|
841
953
|
else
|
842
|
-
conn.pipelined
|
954
|
+
conn.pipelined {
|
843
955
|
procs.each do |key|
|
844
|
-
conn.hget(key,
|
956
|
+
conn.hget(key, "busy")
|
845
957
|
end
|
846
|
-
|
958
|
+
}.sum(&:to_i)
|
847
959
|
end
|
848
960
|
end
|
849
961
|
end
|
850
962
|
end
|
851
|
-
|
852
963
|
end
|