sidekiq 2.15.1 → 4.2.10
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sidekiq might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.github/contributing.md +32 -0
- data/.github/issue_template.md +9 -0
- data/.gitignore +1 -0
- data/.travis.yml +16 -17
- data/3.0-Upgrade.md +70 -0
- data/4.0-Upgrade.md +53 -0
- data/COMM-LICENSE +56 -44
- data/Changes.md +644 -1
- data/Ent-Changes.md +173 -0
- data/Gemfile +27 -0
- data/LICENSE +1 -1
- data/Pro-2.0-Upgrade.md +138 -0
- data/Pro-3.0-Upgrade.md +44 -0
- data/Pro-Changes.md +457 -3
- data/README.md +46 -29
- data/Rakefile +6 -3
- data/bin/sidekiq +4 -0
- data/bin/sidekiqctl +41 -20
- data/bin/sidekiqload +154 -0
- data/code_of_conduct.md +50 -0
- data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
- data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
- data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
- data/lib/generators/sidekiq/worker_generator.rb +49 -0
- data/lib/sidekiq.rb +141 -29
- data/lib/sidekiq/api.rb +540 -106
- data/lib/sidekiq/cli.rb +131 -71
- data/lib/sidekiq/client.rb +168 -96
- data/lib/sidekiq/core_ext.rb +36 -8
- data/lib/sidekiq/exception_handler.rb +20 -28
- data/lib/sidekiq/extensions/action_mailer.rb +25 -5
- data/lib/sidekiq/extensions/active_record.rb +8 -4
- data/lib/sidekiq/extensions/class_methods.rb +9 -5
- data/lib/sidekiq/extensions/generic_proxy.rb +1 -0
- data/lib/sidekiq/fetch.rb +45 -101
- data/lib/sidekiq/launcher.rb +144 -30
- data/lib/sidekiq/logging.rb +69 -12
- data/lib/sidekiq/manager.rb +90 -140
- data/lib/sidekiq/middleware/chain.rb +18 -5
- data/lib/sidekiq/middleware/i18n.rb +9 -2
- data/lib/sidekiq/middleware/server/active_record.rb +1 -1
- data/lib/sidekiq/middleware/server/logging.rb +11 -11
- data/lib/sidekiq/middleware/server/retry_jobs.rb +98 -44
- data/lib/sidekiq/paginator.rb +20 -8
- data/lib/sidekiq/processor.rb +157 -96
- data/lib/sidekiq/rails.rb +109 -5
- data/lib/sidekiq/redis_connection.rb +70 -24
- data/lib/sidekiq/scheduled.rb +122 -50
- data/lib/sidekiq/testing.rb +171 -31
- data/lib/sidekiq/testing/inline.rb +1 -0
- data/lib/sidekiq/util.rb +31 -5
- data/lib/sidekiq/version.rb +2 -1
- data/lib/sidekiq/web.rb +136 -263
- data/lib/sidekiq/web/action.rb +93 -0
- data/lib/sidekiq/web/application.rb +336 -0
- data/lib/sidekiq/web/helpers.rb +278 -0
- data/lib/sidekiq/web/router.rb +100 -0
- data/lib/sidekiq/worker.rb +40 -7
- data/sidekiq.gemspec +18 -14
- data/web/assets/images/favicon.ico +0 -0
- data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
- data/web/assets/javascripts/application.js +67 -19
- data/web/assets/javascripts/dashboard.js +138 -29
- data/web/assets/stylesheets/application.css +267 -406
- data/web/assets/stylesheets/bootstrap.css +4 -8
- data/web/locales/cs.yml +78 -0
- data/web/locales/da.yml +9 -1
- data/web/locales/de.yml +18 -9
- data/web/locales/el.yml +68 -0
- data/web/locales/en.yml +19 -4
- data/web/locales/es.yml +10 -1
- data/web/locales/fa.yml +79 -0
- data/web/locales/fr.yml +50 -32
- data/web/locales/hi.yml +75 -0
- data/web/locales/it.yml +27 -18
- data/web/locales/ja.yml +27 -12
- data/web/locales/ko.yml +8 -3
- data/web/locales/{no.yml → nb.yml} +19 -5
- data/web/locales/nl.yml +8 -3
- data/web/locales/pl.yml +0 -1
- data/web/locales/pt-br.yml +11 -4
- data/web/locales/pt.yml +8 -1
- data/web/locales/ru.yml +39 -21
- data/web/locales/sv.yml +68 -0
- data/web/locales/ta.yml +75 -0
- data/web/locales/uk.yml +76 -0
- data/web/locales/zh-cn.yml +68 -0
- data/web/locales/zh-tw.yml +68 -0
- data/web/views/_footer.erb +17 -0
- data/web/views/_job_info.erb +72 -60
- data/web/views/_nav.erb +58 -25
- data/web/views/_paging.erb +5 -5
- data/web/views/_poll_link.erb +7 -0
- data/web/views/_summary.erb +20 -14
- data/web/views/busy.erb +94 -0
- data/web/views/dashboard.erb +34 -21
- data/web/views/dead.erb +34 -0
- data/web/views/layout.erb +8 -30
- data/web/views/morgue.erb +75 -0
- data/web/views/queue.erb +37 -30
- data/web/views/queues.erb +26 -20
- data/web/views/retries.erb +60 -47
- data/web/views/retry.erb +23 -19
- data/web/views/scheduled.erb +39 -35
- data/web/views/scheduled_job_info.erb +2 -1
- metadata +152 -195
- data/Contributing.md +0 -29
- data/config.ru +0 -18
- data/lib/sidekiq/actor.rb +0 -7
- data/lib/sidekiq/capistrano.rb +0 -54
- data/lib/sidekiq/yaml_patch.rb +0 -21
- data/test/config.yml +0 -11
- data/test/env_based_config.yml +0 -11
- data/test/fake_env.rb +0 -0
- data/test/helper.rb +0 -42
- data/test/test_api.rb +0 -341
- data/test/test_cli.rb +0 -326
- data/test/test_client.rb +0 -211
- data/test/test_exception_handler.rb +0 -124
- data/test/test_extensions.rb +0 -105
- data/test/test_fetch.rb +0 -44
- data/test/test_manager.rb +0 -83
- data/test/test_middleware.rb +0 -135
- data/test/test_processor.rb +0 -160
- data/test/test_redis_connection.rb +0 -97
- data/test/test_retry.rb +0 -306
- data/test/test_scheduled.rb +0 -86
- data/test/test_scheduling.rb +0 -47
- data/test/test_sidekiq.rb +0 -37
- data/test/test_testing.rb +0 -82
- data/test/test_testing_fake.rb +0 -265
- data/test/test_testing_inline.rb +0 -92
- data/test/test_util.rb +0 -18
- data/test/test_web.rb +0 -372
- data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
- data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
- data/web/assets/images/status/active.png +0 -0
- data/web/assets/images/status/idle.png +0 -0
- data/web/assets/javascripts/locales/README.md +0 -27
- data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
- data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.cz.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
- data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
- data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
- data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
- data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
- data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.no.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
- data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
- data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
- data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
- data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
- data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
- data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
- data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
- data/web/assets/javascripts/locales/jquery.timeago.zh-CN.js +0 -20
- data/web/assets/javascripts/locales/jquery.timeago.zh-TW.js +0 -20
- data/web/views/_poll.erb +0 -14
- data/web/views/_workers.erb +0 -29
- data/web/views/index.erb +0 -16
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
<% module_namespacing do -%>
|
3
|
+
class <%= class_name %>WorkerTest < <% if defined? Minitest::Test %>Minitest::Test<% else %>MiniTest::Unit::TestCase<% end %>
|
4
|
+
def test_example
|
5
|
+
skip "add some examples to (or delete) #{__FILE__}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
<% end -%>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rails/generators/named_base'
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Generators # :nodoc:
|
5
|
+
class WorkerGenerator < ::Rails::Generators::NamedBase # :nodoc:
|
6
|
+
desc 'This generator creates a Sidekiq Worker in app/workers and a corresponding test'
|
7
|
+
|
8
|
+
check_class_collision suffix: 'Worker'
|
9
|
+
|
10
|
+
def self.default_generator_root
|
11
|
+
File.dirname(__FILE__)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_worker_file
|
15
|
+
template 'worker.rb.erb', File.join('app/workers', class_path, "#{file_name}_worker.rb")
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_test_file
|
19
|
+
if defined?(RSpec)
|
20
|
+
create_worker_spec
|
21
|
+
else
|
22
|
+
create_worker_test
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def create_worker_spec
|
29
|
+
template_file = File.join(
|
30
|
+
'spec/workers',
|
31
|
+
class_path,
|
32
|
+
"#{file_name}_worker_spec.rb"
|
33
|
+
)
|
34
|
+
template 'worker_spec.rb.erb', template_file
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_worker_test
|
38
|
+
template_file = File.join(
|
39
|
+
'test/workers',
|
40
|
+
class_path,
|
41
|
+
"#{file_name}_worker_test.rb"
|
42
|
+
)
|
43
|
+
template 'worker_test.rb.erb', template_file
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/sidekiq.rb
CHANGED
@@ -1,35 +1,61 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
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
|
+
|
3
6
|
require 'sidekiq/logging'
|
4
7
|
require 'sidekiq/client'
|
5
8
|
require 'sidekiq/worker'
|
6
9
|
require 'sidekiq/redis_connection'
|
7
|
-
require 'sidekiq/util'
|
8
|
-
require 'sidekiq/api'
|
9
10
|
|
10
11
|
require 'json'
|
11
12
|
|
12
13
|
module Sidekiq
|
13
|
-
NAME =
|
14
|
+
NAME = 'Sidekiq'
|
14
15
|
LICENSE = 'See LICENSE and the LGPL-3.0 for licensing details.'
|
15
16
|
|
16
17
|
DEFAULTS = {
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
18
|
+
queues: [],
|
19
|
+
labels: [],
|
20
|
+
concurrency: 25,
|
21
|
+
require: '.',
|
22
|
+
environment: nil,
|
23
|
+
timeout: 8,
|
24
|
+
poll_interval_average: nil,
|
25
|
+
average_scheduled_poll_interval: 15,
|
26
|
+
error_handlers: [],
|
27
|
+
lifecycle_events: {
|
28
|
+
startup: [],
|
29
|
+
quiet: [],
|
30
|
+
shutdown: [],
|
31
|
+
heartbeat: [],
|
32
|
+
},
|
33
|
+
dead_max_jobs: 10_000,
|
34
|
+
dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
|
35
|
+
reloader: proc { |&block| block.call },
|
36
|
+
executor: proc { |&block| block.call },
|
37
|
+
}
|
38
|
+
|
39
|
+
DEFAULT_WORKER_OPTIONS = {
|
40
|
+
'retry' => true,
|
41
|
+
'queue' => 'default'
|
23
42
|
}
|
24
43
|
|
44
|
+
FAKE_INFO = {
|
45
|
+
"redis_version" => "9.9.9",
|
46
|
+
"uptime_in_days" => "9999",
|
47
|
+
"connected_clients" => "9999",
|
48
|
+
"used_memory_human" => "9P",
|
49
|
+
"used_memory_peak_human" => "9P"
|
50
|
+
}.freeze
|
51
|
+
|
25
52
|
def self.❨╯°□°❩╯︵┻━┻
|
26
|
-
puts "Calm down,
|
53
|
+
puts "Calm down, yo."
|
27
54
|
end
|
28
55
|
|
29
56
|
def self.options
|
30
57
|
@options ||= DEFAULTS.dup
|
31
58
|
end
|
32
|
-
|
33
59
|
def self.options=(opts)
|
34
60
|
@options = opts
|
35
61
|
end
|
@@ -61,46 +87,95 @@ module Sidekiq
|
|
61
87
|
defined?(Sidekiq::CLI)
|
62
88
|
end
|
63
89
|
|
64
|
-
def self.redis
|
65
|
-
raise ArgumentError, "requires a block"
|
66
|
-
|
67
|
-
|
90
|
+
def self.redis
|
91
|
+
raise ArgumentError, "requires a block" unless block_given?
|
92
|
+
redis_pool.with do |conn|
|
93
|
+
retryable = true
|
94
|
+
begin
|
95
|
+
yield conn
|
96
|
+
rescue Redis::CommandError => ex
|
97
|
+
#2550 Failover can cause the server to become a slave, need
|
98
|
+
# to disconnect and reopen the socket to get back to the master.
|
99
|
+
(conn.disconnect!; retryable = false; retry) if retryable && ex.message =~ /READONLY/
|
100
|
+
raise
|
101
|
+
end
|
102
|
+
end
|
68
103
|
end
|
69
104
|
|
70
|
-
def self.
|
71
|
-
|
105
|
+
def self.redis_info
|
106
|
+
redis do |conn|
|
107
|
+
begin
|
108
|
+
# admin commands can't go through redis-namespace starting
|
109
|
+
# in redis-namespace 2.0
|
110
|
+
if conn.respond_to?(:namespace)
|
111
|
+
conn.redis.info
|
112
|
+
else
|
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
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
72
122
|
|
73
|
-
|
74
|
-
|
123
|
+
def self.redis_pool
|
124
|
+
@redis ||= Sidekiq::RedisConnection.create
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.redis=(hash)
|
128
|
+
@redis = if hash.is_a?(ConnectionPool)
|
129
|
+
hash
|
75
130
|
else
|
76
|
-
|
131
|
+
Sidekiq::RedisConnection.create(hash)
|
77
132
|
end
|
78
133
|
end
|
79
134
|
|
80
135
|
def self.client_middleware
|
81
|
-
@client_chain ||=
|
136
|
+
@client_chain ||= Middleware::Chain.new
|
82
137
|
yield @client_chain if block_given?
|
83
138
|
@client_chain
|
84
139
|
end
|
85
140
|
|
86
141
|
def self.server_middleware
|
87
|
-
@server_chain ||=
|
142
|
+
@server_chain ||= default_server_middleware
|
88
143
|
yield @server_chain if block_given?
|
89
144
|
@server_chain
|
90
145
|
end
|
91
146
|
|
92
|
-
def self.
|
93
|
-
|
147
|
+
def self.default_server_middleware
|
148
|
+
require 'sidekiq/middleware/server/retry_jobs'
|
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
|
94
155
|
end
|
95
156
|
|
157
|
+
def self.default_worker_options=(hash)
|
158
|
+
@default_worker_options = default_worker_options.merge(hash.stringify_keys)
|
159
|
+
end
|
96
160
|
def self.default_worker_options
|
97
|
-
@default_worker_options
|
161
|
+
defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
|
162
|
+
end
|
163
|
+
|
164
|
+
# Sidekiq.configure_server do |config|
|
165
|
+
# config.default_retries_exhausted = -> (job, ex) do
|
166
|
+
# end
|
167
|
+
# end
|
168
|
+
def self.default_retries_exhausted=(prok)
|
169
|
+
@default_retries_exhausted = prok
|
170
|
+
end
|
171
|
+
@default_retries_exhausted = ->(job, ex) { }
|
172
|
+
def self.default_retries_exhausted
|
173
|
+
@default_retries_exhausted
|
98
174
|
end
|
99
175
|
|
100
176
|
def self.load_json(string)
|
101
177
|
JSON.parse(string)
|
102
178
|
end
|
103
|
-
|
104
179
|
def self.dump_json(object)
|
105
180
|
JSON.generate(object)
|
106
181
|
end
|
@@ -108,15 +183,52 @@ module Sidekiq
|
|
108
183
|
def self.logger
|
109
184
|
Sidekiq::Logging.logger
|
110
185
|
end
|
111
|
-
|
112
186
|
def self.logger=(log)
|
113
187
|
Sidekiq::Logging.logger = log
|
114
188
|
end
|
115
189
|
|
116
|
-
|
117
|
-
|
190
|
+
# How frequently Redis should be checked by a random Sidekiq process for
|
191
|
+
# scheduled and retriable jobs. Each individual process will take turns by
|
192
|
+
# waiting some multiple of this value.
|
193
|
+
#
|
194
|
+
# See sidekiq/scheduled.rb for an in-depth explanation of this value
|
195
|
+
def self.average_scheduled_poll_interval=(interval)
|
196
|
+
self.options[:average_scheduled_poll_interval] = interval
|
118
197
|
end
|
119
198
|
|
199
|
+
# Register a proc to handle any error which occurs within the Sidekiq process.
|
200
|
+
#
|
201
|
+
# Sidekiq.configure_server do |config|
|
202
|
+
# config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# The default error handler logs errors to Sidekiq.logger.
|
206
|
+
def self.error_handlers
|
207
|
+
self.options[:error_handlers]
|
208
|
+
end
|
209
|
+
|
210
|
+
# Register a block to run at a point in the Sidekiq lifecycle.
|
211
|
+
# :startup, :quiet or :shutdown are valid events.
|
212
|
+
#
|
213
|
+
# Sidekiq.configure_server do |config|
|
214
|
+
# config.on(:shutdown) do
|
215
|
+
# puts "Goodbye cruel world!"
|
216
|
+
# end
|
217
|
+
# end
|
218
|
+
def self.on(event, &block)
|
219
|
+
raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
|
220
|
+
raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
|
221
|
+
options[:lifecycle_events][event] << block
|
222
|
+
end
|
223
|
+
|
224
|
+
# We are shutting down Sidekiq but what about workers that
|
225
|
+
# are working on some long job? This error is
|
226
|
+
# raised in workers that have not finished within the hard
|
227
|
+
# timeout limit. This is needed to rollback db transactions,
|
228
|
+
# otherwise Ruby's Thread#kill will commit. See #377.
|
229
|
+
# DO NOT RESCUE THIS ERROR IN YOUR WORKERS
|
230
|
+
class Shutdown < Interrupt; end
|
231
|
+
|
120
232
|
end
|
121
233
|
|
122
234
|
require 'sidekiq/extensions/class_methods'
|
data/lib/sidekiq/api.rb
CHANGED
@@ -1,45 +1,142 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
1
3
|
require 'sidekiq'
|
2
4
|
|
3
5
|
module Sidekiq
|
4
6
|
class Stats
|
7
|
+
def initialize
|
8
|
+
fetch_stats!
|
9
|
+
end
|
10
|
+
|
5
11
|
def processed
|
6
|
-
|
12
|
+
stat :processed
|
7
13
|
end
|
8
14
|
|
9
15
|
def failed
|
10
|
-
|
16
|
+
stat :failed
|
11
17
|
end
|
12
18
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
19
|
+
def scheduled_size
|
20
|
+
stat :scheduled_size
|
21
|
+
end
|
22
|
+
|
23
|
+
def retry_size
|
24
|
+
stat :retry_size
|
25
|
+
end
|
26
|
+
|
27
|
+
def dead_size
|
28
|
+
stat :dead_size
|
29
|
+
end
|
30
|
+
|
31
|
+
def enqueued
|
32
|
+
stat :enqueued
|
33
|
+
end
|
34
|
+
|
35
|
+
def processes_size
|
36
|
+
stat :processes_size
|
37
|
+
end
|
38
|
+
|
39
|
+
def workers_size
|
40
|
+
stat :workers_size
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_queue_latency
|
44
|
+
stat :default_queue_latency
|
18
45
|
end
|
19
46
|
|
20
47
|
def queues
|
21
|
-
Sidekiq.
|
22
|
-
|
48
|
+
Sidekiq::Stats::Queues.new.lengths
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_stats!
|
52
|
+
pipe1_res = Sidekiq.redis do |conn|
|
53
|
+
conn.pipelined do
|
54
|
+
conn.get('stat:processed'.freeze)
|
55
|
+
conn.get('stat:failed'.freeze)
|
56
|
+
conn.zcard('schedule'.freeze)
|
57
|
+
conn.zcard('retry'.freeze)
|
58
|
+
conn.zcard('dead'.freeze)
|
59
|
+
conn.scard('processes'.freeze)
|
60
|
+
conn.lrange('queue:default'.freeze, -1, -1)
|
61
|
+
conn.smembers('processes'.freeze)
|
62
|
+
conn.smembers('queues'.freeze)
|
63
|
+
end
|
64
|
+
end
|
23
65
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
66
|
+
pipe2_res = Sidekiq.redis do |conn|
|
67
|
+
conn.pipelined do
|
68
|
+
pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
|
69
|
+
pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
|
70
|
+
end
|
71
|
+
end
|
28
72
|
|
29
|
-
|
73
|
+
s = pipe1_res[7].size
|
74
|
+
workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
|
75
|
+
enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
|
76
|
+
|
77
|
+
default_queue_latency = if (entry = pipe1_res[6].first)
|
78
|
+
job = Sidekiq.load_json(entry)
|
79
|
+
now = Time.now.to_f
|
80
|
+
thence = job['enqueued_at'.freeze] || now
|
81
|
+
now - thence
|
82
|
+
else
|
83
|
+
0
|
84
|
+
end
|
85
|
+
@stats = {
|
86
|
+
processed: pipe1_res[0].to_i,
|
87
|
+
failed: pipe1_res[1].to_i,
|
88
|
+
scheduled_size: pipe1_res[2],
|
89
|
+
retry_size: pipe1_res[3],
|
90
|
+
dead_size: pipe1_res[4],
|
91
|
+
processes_size: pipe1_res[5],
|
92
|
+
|
93
|
+
default_queue_latency: default_queue_latency,
|
94
|
+
workers_size: workers_size,
|
95
|
+
enqueued: enqueued
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def reset(*stats)
|
100
|
+
all = %w(failed processed)
|
101
|
+
stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
|
102
|
+
|
103
|
+
mset_args = []
|
104
|
+
stats.each do |stat|
|
105
|
+
mset_args << "stat:#{stat}"
|
106
|
+
mset_args << 0
|
107
|
+
end
|
108
|
+
Sidekiq.redis do |conn|
|
109
|
+
conn.mset(*mset_args)
|
30
110
|
end
|
31
111
|
end
|
32
112
|
|
33
|
-
|
34
|
-
queues.values.inject(&:+) || 0
|
35
|
-
end
|
113
|
+
private
|
36
114
|
|
37
|
-
def
|
38
|
-
|
115
|
+
def stat(s)
|
116
|
+
@stats[s]
|
39
117
|
end
|
40
118
|
|
41
|
-
|
42
|
-
|
119
|
+
class Queues
|
120
|
+
def lengths
|
121
|
+
Sidekiq.redis do |conn|
|
122
|
+
queues = conn.smembers('queues'.freeze)
|
123
|
+
|
124
|
+
lengths = conn.pipelined do
|
125
|
+
queues.each do |queue|
|
126
|
+
conn.llen("queue:#{queue}")
|
127
|
+
end
|
128
|
+
end
|
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 }
|
136
|
+
|
137
|
+
Hash[array_of_arrays.reverse]
|
138
|
+
end
|
139
|
+
end
|
43
140
|
end
|
44
141
|
|
45
142
|
class History
|
@@ -61,15 +158,20 @@ module Sidekiq
|
|
61
158
|
def date_stat_hash(stat)
|
62
159
|
i = 0
|
63
160
|
stat_hash = {}
|
161
|
+
keys = []
|
162
|
+
dates = []
|
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
|
64
171
|
|
65
172
|
Sidekiq.redis do |conn|
|
66
|
-
|
67
|
-
|
68
|
-
value = conn.get("stat:#{stat}:#{date}")
|
69
|
-
|
70
|
-
stat_hash[date.to_s] = value ? value.to_i : 0
|
71
|
-
|
72
|
-
i += 1
|
173
|
+
conn.mget(keys).each_with_index do |value, idx|
|
174
|
+
stat_hash[dates[idx]] = value ? value.to_i : 0
|
73
175
|
end
|
74
176
|
end
|
75
177
|
|
@@ -93,8 +195,11 @@ module Sidekiq
|
|
93
195
|
class Queue
|
94
196
|
include Enumerable
|
95
197
|
|
198
|
+
##
|
199
|
+
# Return all known queues within Redis.
|
200
|
+
#
|
96
201
|
def self.all
|
97
|
-
Sidekiq.redis {|c| c.smembers('queues') }.map {|q| Sidekiq::Queue.new(q) }
|
202
|
+
Sidekiq.redis { |c| c.smembers('queues'.freeze) }.sort.map { |q| Sidekiq::Queue.new(q) }
|
98
203
|
end
|
99
204
|
|
100
205
|
attr_reader :name
|
@@ -108,42 +213,66 @@ module Sidekiq
|
|
108
213
|
Sidekiq.redis { |con| con.llen(@rname) }
|
109
214
|
end
|
110
215
|
|
216
|
+
# Sidekiq Pro overrides this
|
217
|
+
def paused?
|
218
|
+
false
|
219
|
+
end
|
220
|
+
|
221
|
+
##
|
222
|
+
# Calculates this queue's latency, the difference in seconds since the oldest
|
223
|
+
# job in the queue was enqueued.
|
224
|
+
#
|
225
|
+
# @return Float
|
111
226
|
def latency
|
112
227
|
entry = Sidekiq.redis do |conn|
|
113
228
|
conn.lrange(@rname, -1, -1)
|
114
229
|
end.first
|
115
230
|
return 0 unless entry
|
116
|
-
|
231
|
+
job = Sidekiq.load_json(entry)
|
232
|
+
now = Time.now.to_f
|
233
|
+
thence = job['enqueued_at'] || now
|
234
|
+
now - thence
|
117
235
|
end
|
118
236
|
|
119
|
-
def each
|
237
|
+
def each
|
238
|
+
initial_size = size
|
239
|
+
deleted_size = 0
|
120
240
|
page = 0
|
121
241
|
page_size = 50
|
122
242
|
|
123
|
-
|
243
|
+
while true do
|
244
|
+
range_start = page * page_size - deleted_size
|
245
|
+
range_end = range_start + page_size - 1
|
124
246
|
entries = Sidekiq.redis do |conn|
|
125
|
-
conn.lrange @rname,
|
247
|
+
conn.lrange @rname, range_start, range_end
|
126
248
|
end
|
127
249
|
break if entries.empty?
|
128
250
|
page += 1
|
129
251
|
entries.each do |entry|
|
130
|
-
|
252
|
+
yield Job.new(entry, @name)
|
131
253
|
end
|
254
|
+
deleted_size = initial_size - size
|
132
255
|
end
|
133
256
|
end
|
134
257
|
|
258
|
+
##
|
259
|
+
# Find the job with the given JID within this queue.
|
260
|
+
#
|
261
|
+
# This is a slow, inefficient operation. Do not use under
|
262
|
+
# normal conditions. Sidekiq Pro contains a faster version.
|
135
263
|
def find_job(jid)
|
136
|
-
|
264
|
+
detect { |j| j.jid == jid }
|
137
265
|
end
|
138
266
|
|
139
267
|
def clear
|
140
268
|
Sidekiq.redis do |conn|
|
141
269
|
conn.multi do
|
142
270
|
conn.del(@rname)
|
143
|
-
conn.srem("queues", name)
|
271
|
+
conn.srem("queues".freeze, name)
|
144
272
|
end
|
145
273
|
end
|
146
274
|
end
|
275
|
+
alias_method :💣, :clear
|
147
276
|
end
|
148
277
|
|
149
278
|
##
|
@@ -155,10 +284,11 @@ module Sidekiq
|
|
155
284
|
#
|
156
285
|
class Job
|
157
286
|
attr_reader :item
|
287
|
+
attr_reader :value
|
158
288
|
|
159
289
|
def initialize(item, queue_name=nil)
|
160
290
|
@value = item
|
161
|
-
@item = Sidekiq.load_json(item)
|
291
|
+
@item = item.is_a?(Hash) ? item : Sidekiq.load_json(item)
|
162
292
|
@queue = queue_name || @item['queue']
|
163
293
|
end
|
164
294
|
|
@@ -166,6 +296,46 @@ module Sidekiq
|
|
166
296
|
@item['class']
|
167
297
|
end
|
168
298
|
|
299
|
+
def display_class
|
300
|
+
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
301
|
+
@klass ||= case klass
|
302
|
+
when /\ASidekiq::Extensions::Delayed/
|
303
|
+
safe_load(args[0], klass) do |target, method, _|
|
304
|
+
"#{target}.#{method}"
|
305
|
+
end
|
306
|
+
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
307
|
+
job_class = @item['wrapped'] || args[0]
|
308
|
+
if 'ActionMailer::DeliveryJob' == job_class
|
309
|
+
# MailerClass#mailer_method
|
310
|
+
args[0]['arguments'][0..1].join('#')
|
311
|
+
else
|
312
|
+
job_class
|
313
|
+
end
|
314
|
+
else
|
315
|
+
klass
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def display_args
|
320
|
+
# Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
|
321
|
+
@args ||= case klass
|
322
|
+
when /\ASidekiq::Extensions::Delayed/
|
323
|
+
safe_load(args[0], args) do |_, _, arg|
|
324
|
+
arg
|
325
|
+
end
|
326
|
+
when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
|
327
|
+
job_args = @item['wrapped'] ? args[0]["arguments"] : []
|
328
|
+
if 'ActionMailer::DeliveryJob' == (@item['wrapped'] || args[0])
|
329
|
+
# remove MailerClass, mailer_method and 'deliver_now'
|
330
|
+
job_args.drop(3)
|
331
|
+
else
|
332
|
+
job_args
|
333
|
+
end
|
334
|
+
else
|
335
|
+
args
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
169
339
|
def args
|
170
340
|
@item['args']
|
171
341
|
end
|
@@ -175,7 +345,11 @@ module Sidekiq
|
|
175
345
|
end
|
176
346
|
|
177
347
|
def enqueued_at
|
178
|
-
Time.at(@item['enqueued_at']
|
348
|
+
@item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
|
349
|
+
end
|
350
|
+
|
351
|
+
def created_at
|
352
|
+
Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
|
179
353
|
end
|
180
354
|
|
181
355
|
def queue
|
@@ -183,25 +357,40 @@ module Sidekiq
|
|
183
357
|
end
|
184
358
|
|
185
359
|
def latency
|
186
|
-
Time.now.to_f
|
360
|
+
now = Time.now.to_f
|
361
|
+
now - (@item['enqueued_at'] || @item['created_at'] || now)
|
187
362
|
end
|
188
363
|
|
189
364
|
##
|
190
365
|
# Remove this job from the queue.
|
191
366
|
def delete
|
192
367
|
count = Sidekiq.redis do |conn|
|
193
|
-
conn.lrem("queue:#{@queue}",
|
368
|
+
conn.lrem("queue:#{@queue}", 1, @value)
|
194
369
|
end
|
195
370
|
count != 0
|
196
371
|
end
|
197
372
|
|
198
373
|
def [](name)
|
199
|
-
@item
|
374
|
+
@item[name]
|
375
|
+
end
|
376
|
+
|
377
|
+
private
|
378
|
+
|
379
|
+
def safe_load(content, default)
|
380
|
+
begin
|
381
|
+
yield(*YAML.load(content))
|
382
|
+
rescue => ex
|
383
|
+
# #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
|
384
|
+
# memory yet so the YAML can't be loaded.
|
385
|
+
Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == 'development'
|
386
|
+
default
|
387
|
+
end
|
200
388
|
end
|
201
389
|
end
|
202
390
|
|
203
391
|
class SortedEntry < Job
|
204
392
|
attr_reader :score
|
393
|
+
attr_reader :parent
|
205
394
|
|
206
395
|
def initialize(parent, score, item)
|
207
396
|
super(item)
|
@@ -214,76 +403,144 @@ module Sidekiq
|
|
214
403
|
end
|
215
404
|
|
216
405
|
def delete
|
217
|
-
@
|
406
|
+
if @value
|
407
|
+
@parent.delete_by_value(@parent.name, @value)
|
408
|
+
else
|
409
|
+
@parent.delete_by_jid(score, jid)
|
410
|
+
end
|
218
411
|
end
|
219
412
|
|
220
413
|
def reschedule(at)
|
221
|
-
|
414
|
+
delete
|
222
415
|
@parent.schedule(at, item)
|
223
416
|
end
|
224
417
|
|
225
418
|
def add_to_queue
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
results.map do |message|
|
230
|
-
msg = Sidekiq.load_json(message)
|
231
|
-
Sidekiq::Client.push(msg)
|
232
|
-
end
|
419
|
+
remove_job do |message|
|
420
|
+
msg = Sidekiq.load_json(message)
|
421
|
+
Sidekiq::Client.push(msg)
|
233
422
|
end
|
234
423
|
end
|
235
424
|
|
236
425
|
def retry
|
237
|
-
|
426
|
+
remove_job do |message|
|
427
|
+
msg = Sidekiq.load_json(message)
|
428
|
+
msg['retry_count'] -= 1 if msg['retry_count']
|
429
|
+
Sidekiq::Client.push(msg)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
##
|
434
|
+
# Place job in the dead set
|
435
|
+
def kill
|
436
|
+
remove_job do |message|
|
437
|
+
now = Time.now.to_f
|
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
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
def error?
|
449
|
+
!!item['error_class']
|
450
|
+
end
|
451
|
+
|
452
|
+
private
|
453
|
+
|
454
|
+
def remove_job
|
238
455
|
Sidekiq.redis do |conn|
|
239
|
-
results = conn.
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
456
|
+
results = conn.multi do
|
457
|
+
conn.zrangebyscore(parent.name, score, score)
|
458
|
+
conn.zremrangebyscore(parent.name, score, score)
|
459
|
+
end.first
|
460
|
+
|
461
|
+
if results.size == 1
|
462
|
+
yield results.first
|
463
|
+
else
|
464
|
+
# multiple jobs with the same score
|
465
|
+
# find the one with the right JID and push it
|
466
|
+
hash = results.group_by do |message|
|
467
|
+
if message.index(jid)
|
468
|
+
msg = Sidekiq.load_json(message)
|
469
|
+
msg['jid'] == jid
|
470
|
+
else
|
471
|
+
false
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
msg = hash.fetch(true, []).first
|
476
|
+
yield msg if msg
|
477
|
+
|
478
|
+
# push the rest back onto the sorted set
|
479
|
+
conn.multi do
|
480
|
+
hash.fetch(false, []).each do |message|
|
481
|
+
conn.zadd(parent.name, score.to_f.to_s, message)
|
482
|
+
end
|
483
|
+
end
|
245
484
|
end
|
246
485
|
end
|
247
486
|
end
|
487
|
+
|
248
488
|
end
|
249
489
|
|
250
490
|
class SortedSet
|
251
491
|
include Enumerable
|
252
492
|
|
493
|
+
attr_reader :name
|
494
|
+
|
253
495
|
def initialize(name)
|
254
|
-
@
|
496
|
+
@name = name
|
497
|
+
@_size = size
|
255
498
|
end
|
256
499
|
|
257
500
|
def size
|
258
|
-
Sidekiq.redis {|c| c.zcard(
|
501
|
+
Sidekiq.redis { |c| c.zcard(name) }
|
259
502
|
end
|
260
503
|
|
504
|
+
def clear
|
505
|
+
Sidekiq.redis do |conn|
|
506
|
+
conn.del(name)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
alias_method :💣, :clear
|
510
|
+
end
|
511
|
+
|
512
|
+
class JobSet < SortedSet
|
513
|
+
|
261
514
|
def schedule(timestamp, message)
|
262
515
|
Sidekiq.redis do |conn|
|
263
|
-
conn.zadd(
|
516
|
+
conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
|
264
517
|
end
|
265
518
|
end
|
266
519
|
|
267
|
-
def each
|
268
|
-
|
520
|
+
def each
|
521
|
+
initial_size = @_size
|
522
|
+
offset_size = 0
|
269
523
|
page = -1
|
270
524
|
page_size = 50
|
271
525
|
|
272
|
-
|
526
|
+
while true do
|
527
|
+
range_start = page * page_size + offset_size
|
528
|
+
range_end = range_start + page_size - 1
|
273
529
|
elements = Sidekiq.redis do |conn|
|
274
|
-
conn.zrange
|
530
|
+
conn.zrange name, range_start, range_end, with_scores: true
|
275
531
|
end
|
276
532
|
break if elements.empty?
|
277
533
|
page -= 1
|
278
534
|
elements.each do |element, score|
|
279
|
-
|
535
|
+
yield SortedEntry.new(self, score, element)
|
280
536
|
end
|
537
|
+
offset_size = initial_size - @_size
|
281
538
|
end
|
282
539
|
end
|
283
540
|
|
284
541
|
def fetch(score, jid = nil)
|
285
542
|
elements = Sidekiq.redis do |conn|
|
286
|
-
conn.zrangebyscore(
|
543
|
+
conn.zrangebyscore(name, score, score)
|
287
544
|
end
|
288
545
|
|
289
546
|
elements.inject([]) do |result, element|
|
@@ -297,52 +554,54 @@ module Sidekiq
|
|
297
554
|
end
|
298
555
|
end
|
299
556
|
|
557
|
+
##
|
558
|
+
# 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.
|
300
562
|
def find_job(jid)
|
301
563
|
self.detect { |j| j.jid == jid }
|
302
564
|
end
|
303
565
|
|
304
|
-
def
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
566
|
+
def delete_by_value(name, value)
|
567
|
+
Sidekiq.redis do |conn|
|
568
|
+
ret = conn.zrem(name, value)
|
569
|
+
@_size -= 1 if ret
|
570
|
+
ret
|
571
|
+
end
|
572
|
+
end
|
309
573
|
|
310
|
-
|
574
|
+
def delete_by_jid(score, jid)
|
575
|
+
Sidekiq.redis do |conn|
|
576
|
+
elements = conn.zrangebyscore(name, score, score)
|
577
|
+
elements.each do |element|
|
311
578
|
message = Sidekiq.load_json(element)
|
312
|
-
|
313
579
|
if message["jid"] == jid
|
314
|
-
|
580
|
+
ret = conn.zrem(name, element)
|
581
|
+
@_size -= 1 if ret
|
582
|
+
break ret
|
315
583
|
end
|
584
|
+
false
|
316
585
|
end
|
317
|
-
elements_with_jid.count != 0
|
318
|
-
else
|
319
|
-
count = Sidekiq.redis do |conn|
|
320
|
-
conn.zremrangebyscore(@zset, score, score)
|
321
|
-
end
|
322
|
-
count != 0
|
323
586
|
end
|
324
587
|
end
|
325
588
|
|
326
|
-
|
327
|
-
Sidekiq.redis do |conn|
|
328
|
-
conn.del(@zset)
|
329
|
-
end
|
330
|
-
end
|
589
|
+
alias_method :delete, :delete_by_jid
|
331
590
|
end
|
332
591
|
|
333
592
|
##
|
334
593
|
# Allows enumeration of scheduled jobs within Sidekiq.
|
335
594
|
# Based on this, you can search/filter for jobs. Here's an
|
336
595
|
# example where I'm selecting all jobs of a certain type
|
337
|
-
# and deleting them from the
|
596
|
+
# and deleting them from the schedule queue.
|
338
597
|
#
|
339
598
|
# r = Sidekiq::ScheduledSet.new
|
340
|
-
# r.select do |
|
341
|
-
#
|
342
|
-
#
|
343
|
-
#
|
599
|
+
# r.select do |scheduled|
|
600
|
+
# scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
|
601
|
+
# scheduled.args[0] == 'User' &&
|
602
|
+
# scheduled.args[1] == 'setup_new_subscriber'
|
344
603
|
# end.map(&:delete)
|
345
|
-
class ScheduledSet <
|
604
|
+
class ScheduledSet < JobSet
|
346
605
|
def initialize
|
347
606
|
super 'schedule'
|
348
607
|
end
|
@@ -360,7 +619,7 @@ module Sidekiq
|
|
360
619
|
# retri.args[0] == 'User' &&
|
361
620
|
# retri.args[1] == 'setup_new_subscriber'
|
362
621
|
# end.map(&:delete)
|
363
|
-
class RetrySet <
|
622
|
+
class RetrySet < JobSet
|
364
623
|
def initialize
|
365
624
|
super 'retry'
|
366
625
|
end
|
@@ -372,8 +631,165 @@ module Sidekiq
|
|
372
631
|
end
|
373
632
|
end
|
374
633
|
|
634
|
+
##
|
635
|
+
# Allows enumeration of dead jobs within Sidekiq.
|
636
|
+
#
|
637
|
+
class DeadSet < JobSet
|
638
|
+
def initialize
|
639
|
+
super 'dead'
|
640
|
+
end
|
641
|
+
|
642
|
+
def retry_all
|
643
|
+
while size > 0
|
644
|
+
each(&:retry)
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
def self.max_jobs
|
649
|
+
Sidekiq.options[:dead_max_jobs]
|
650
|
+
end
|
651
|
+
|
652
|
+
def self.timeout
|
653
|
+
Sidekiq.options[:dead_timeout_in_seconds]
|
654
|
+
end
|
655
|
+
end
|
375
656
|
|
376
657
|
##
|
658
|
+
# Enumerates the set of Sidekiq processes which are actively working
|
659
|
+
# right now. Each process send a heartbeat to Redis every 5 seconds
|
660
|
+
# so this set should be relatively accurate, barring network partitions.
|
661
|
+
#
|
662
|
+
# Yields a Sidekiq::Process.
|
663
|
+
#
|
664
|
+
class ProcessSet
|
665
|
+
include Enumerable
|
666
|
+
|
667
|
+
def initialize(clean_plz=true)
|
668
|
+
self.class.cleanup if clean_plz
|
669
|
+
end
|
670
|
+
|
671
|
+
# Cleans up dead processes recorded in Redis.
|
672
|
+
# Returns the number of processes cleaned.
|
673
|
+
def self.cleanup
|
674
|
+
count = 0
|
675
|
+
Sidekiq.redis do |conn|
|
676
|
+
procs = conn.smembers('processes').sort
|
677
|
+
heartbeats = conn.pipelined do
|
678
|
+
procs.each do |key|
|
679
|
+
conn.hget(key, 'info')
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
# the hash named key has an expiry of 60 seconds.
|
684
|
+
# if it's not found, that means the process has not reported
|
685
|
+
# in to Redis and probably died.
|
686
|
+
to_prune = []
|
687
|
+
heartbeats.each_with_index do |beat, i|
|
688
|
+
to_prune << procs[i] if beat.nil?
|
689
|
+
end
|
690
|
+
count = conn.srem('processes', to_prune) unless to_prune.empty?
|
691
|
+
end
|
692
|
+
count
|
693
|
+
end
|
694
|
+
|
695
|
+
def each
|
696
|
+
procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
|
697
|
+
|
698
|
+
Sidekiq.redis do |conn|
|
699
|
+
# We're making a tradeoff here between consuming more memory instead of
|
700
|
+
# making more roundtrips to Redis, but if you have hundreds or thousands of workers,
|
701
|
+
# you'll be happier this way
|
702
|
+
result = conn.pipelined do
|
703
|
+
procs.each do |key|
|
704
|
+
conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
result.each do |info, busy, at_s, quiet|
|
709
|
+
hash = Sidekiq.load_json(info)
|
710
|
+
yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
nil
|
715
|
+
end
|
716
|
+
|
717
|
+
# This method is not guaranteed accurate since it does not prune the set
|
718
|
+
# based on current heartbeat. #each does that and ensures the set only
|
719
|
+
# contains Sidekiq processes which have sent a heartbeat within the last
|
720
|
+
# 60 seconds.
|
721
|
+
def size
|
722
|
+
Sidekiq.redis { |conn| conn.scard('processes') }
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
#
|
727
|
+
# Sidekiq::Process represents an active Sidekiq process talking with Redis.
|
728
|
+
# Each process has a set of attributes which look like this:
|
729
|
+
#
|
730
|
+
# {
|
731
|
+
# 'hostname' => 'app-1.example.com',
|
732
|
+
# 'started_at' => <process start time>,
|
733
|
+
# 'pid' => 12345,
|
734
|
+
# 'tag' => 'myapp'
|
735
|
+
# 'concurrency' => 25,
|
736
|
+
# 'queues' => ['default', 'low'],
|
737
|
+
# 'busy' => 10,
|
738
|
+
# 'beat' => <last heartbeat>,
|
739
|
+
# 'identity' => <unique string identifying the process>,
|
740
|
+
# }
|
741
|
+
class Process
|
742
|
+
def initialize(hash)
|
743
|
+
@attribs = hash
|
744
|
+
end
|
745
|
+
|
746
|
+
def tag
|
747
|
+
self['tag']
|
748
|
+
end
|
749
|
+
|
750
|
+
def labels
|
751
|
+
Array(self['labels'])
|
752
|
+
end
|
753
|
+
|
754
|
+
def [](key)
|
755
|
+
@attribs[key]
|
756
|
+
end
|
757
|
+
|
758
|
+
def quiet!
|
759
|
+
signal('USR1')
|
760
|
+
end
|
761
|
+
|
762
|
+
def stop!
|
763
|
+
signal('TERM')
|
764
|
+
end
|
765
|
+
|
766
|
+
def dump_threads
|
767
|
+
signal('TTIN')
|
768
|
+
end
|
769
|
+
|
770
|
+
def stopping?
|
771
|
+
self['quiet'] == 'true'
|
772
|
+
end
|
773
|
+
|
774
|
+
private
|
775
|
+
|
776
|
+
def signal(sig)
|
777
|
+
key = "#{identity}-signals"
|
778
|
+
Sidekiq.redis do |c|
|
779
|
+
c.multi do
|
780
|
+
c.lpush(key, sig)
|
781
|
+
c.expire(key, 60)
|
782
|
+
end
|
783
|
+
end
|
784
|
+
end
|
785
|
+
|
786
|
+
def identity
|
787
|
+
self['identity']
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
##
|
792
|
+
# A worker is a thread that is currently processing a job.
|
377
793
|
# Programmatic access to the current active worker set.
|
378
794
|
#
|
379
795
|
# WARNING WARNING WARNING
|
@@ -384,34 +800,52 @@ module Sidekiq
|
|
384
800
|
#
|
385
801
|
# workers = Sidekiq::Workers.new
|
386
802
|
# workers.size => 2
|
387
|
-
# workers.each do |
|
388
|
-
# #
|
803
|
+
# workers.each do |process_id, thread_id, work|
|
804
|
+
# # process_id is a unique identifier per Sidekiq process
|
805
|
+
# # thread_id is a unique identifier per thread
|
389
806
|
# # work is a Hash which looks like:
|
390
807
|
# # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
|
391
|
-
# #
|
808
|
+
# # run_at is an epoch Integer.
|
392
809
|
# end
|
393
|
-
|
810
|
+
#
|
394
811
|
class Workers
|
395
812
|
include Enumerable
|
396
813
|
|
397
|
-
def each
|
814
|
+
def each
|
398
815
|
Sidekiq.redis do |conn|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
conn.
|
403
|
-
conn.
|
816
|
+
procs = conn.smembers('processes')
|
817
|
+
procs.sort.each do |key|
|
818
|
+
valid, workers = conn.pipelined do
|
819
|
+
conn.exists(key)
|
820
|
+
conn.hgetall("#{key}:workers")
|
821
|
+
end
|
822
|
+
next unless valid
|
823
|
+
workers.each_pair do |tid, json|
|
824
|
+
yield key, tid, Sidekiq.load_json(json)
|
404
825
|
end
|
405
|
-
next unless msg
|
406
|
-
block.call(w, Sidekiq.load_json(msg), time)
|
407
826
|
end
|
408
827
|
end
|
409
828
|
end
|
410
829
|
|
830
|
+
# Note that #size is only as accurate as Sidekiq's heartbeat,
|
831
|
+
# which happens every 5 seconds. It is NOT real-time.
|
832
|
+
#
|
833
|
+
# Not very efficient if you have lots of Sidekiq
|
834
|
+
# processes but the alternative is a global counter
|
835
|
+
# which can easily get out of sync with crashy processes.
|
411
836
|
def size
|
412
837
|
Sidekiq.redis do |conn|
|
413
|
-
conn.
|
414
|
-
|
838
|
+
procs = conn.smembers('processes')
|
839
|
+
if procs.empty?
|
840
|
+
0
|
841
|
+
else
|
842
|
+
conn.pipelined do
|
843
|
+
procs.each do |key|
|
844
|
+
conn.hget(key, 'busy')
|
845
|
+
end
|
846
|
+
end.map(&:to_i).inject(:+)
|
847
|
+
end
|
848
|
+
end
|
415
849
|
end
|
416
850
|
end
|
417
851
|
|