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.

Files changed (106) hide show
  1. checksums.yaml +5 -5
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
  3. data/.github/workflows/ci.yml +41 -0
  4. data/.gitignore +2 -1
  5. data/.standard.yml +20 -0
  6. data/5.0-Upgrade.md +56 -0
  7. data/6.0-Upgrade.md +72 -0
  8. data/COMM-LICENSE +12 -10
  9. data/Changes.md +354 -1
  10. data/Ent-2.0-Upgrade.md +37 -0
  11. data/Ent-Changes.md +111 -3
  12. data/Gemfile +16 -21
  13. data/Gemfile.lock +192 -0
  14. data/LICENSE +1 -1
  15. data/Pro-4.0-Upgrade.md +35 -0
  16. data/Pro-5.0-Upgrade.md +25 -0
  17. data/Pro-Changes.md +181 -4
  18. data/README.md +19 -33
  19. data/Rakefile +6 -8
  20. data/bin/sidekiq +26 -2
  21. data/bin/sidekiqload +37 -34
  22. data/bin/sidekiqmon +8 -0
  23. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  24. data/lib/generators/sidekiq/templates/worker_test.rb.erb +1 -1
  25. data/lib/generators/sidekiq/worker_generator.rb +21 -13
  26. data/lib/sidekiq.rb +86 -61
  27. data/lib/sidekiq/api.rb +320 -209
  28. data/lib/sidekiq/cli.rb +207 -217
  29. data/lib/sidekiq/client.rb +78 -51
  30. data/lib/sidekiq/delay.rb +41 -0
  31. data/lib/sidekiq/exception_handler.rb +12 -16
  32. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  33. data/lib/sidekiq/extensions/active_record.rb +13 -10
  34. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  35. data/lib/sidekiq/extensions/generic_proxy.rb +10 -4
  36. data/lib/sidekiq/fetch.rb +29 -30
  37. data/lib/sidekiq/job_logger.rb +63 -0
  38. data/lib/sidekiq/job_retry.rb +262 -0
  39. data/lib/sidekiq/launcher.rb +102 -69
  40. data/lib/sidekiq/logger.rb +165 -0
  41. data/lib/sidekiq/manager.rb +16 -19
  42. data/lib/sidekiq/middleware/chain.rb +15 -5
  43. data/lib/sidekiq/middleware/i18n.rb +5 -7
  44. data/lib/sidekiq/monitor.rb +133 -0
  45. data/lib/sidekiq/paginator.rb +18 -14
  46. data/lib/sidekiq/processor.rb +161 -82
  47. data/lib/sidekiq/rails.rb +27 -100
  48. data/lib/sidekiq/redis_connection.rb +60 -20
  49. data/lib/sidekiq/scheduled.rb +61 -35
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +24 -0
  52. data/lib/sidekiq/testing.rb +48 -28
  53. data/lib/sidekiq/testing/inline.rb +2 -1
  54. data/lib/sidekiq/util.rb +20 -16
  55. data/lib/sidekiq/version.rb +2 -1
  56. data/lib/sidekiq/web.rb +57 -57
  57. data/lib/sidekiq/web/action.rb +14 -14
  58. data/lib/sidekiq/web/application.rb +103 -84
  59. data/lib/sidekiq/web/csrf_protection.rb +158 -0
  60. data/lib/sidekiq/web/helpers.rb +126 -71
  61. data/lib/sidekiq/web/router.rb +18 -17
  62. data/lib/sidekiq/worker.rb +164 -41
  63. data/sidekiq.gemspec +15 -27
  64. data/web/assets/javascripts/application.js +25 -27
  65. data/web/assets/javascripts/dashboard.js +33 -37
  66. data/web/assets/stylesheets/application-dark.css +143 -0
  67. data/web/assets/stylesheets/application-rtl.css +246 -0
  68. data/web/assets/stylesheets/application.css +385 -10
  69. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  70. data/web/assets/stylesheets/bootstrap.css +2 -2
  71. data/web/locales/ar.yml +81 -0
  72. data/web/locales/de.yml +14 -2
  73. data/web/locales/en.yml +4 -0
  74. data/web/locales/es.yml +4 -3
  75. data/web/locales/fa.yml +1 -0
  76. data/web/locales/fr.yml +2 -2
  77. data/web/locales/he.yml +79 -0
  78. data/web/locales/ja.yml +9 -4
  79. data/web/locales/lt.yml +83 -0
  80. data/web/locales/pl.yml +4 -4
  81. data/web/locales/ru.yml +4 -0
  82. data/web/locales/ur.yml +80 -0
  83. data/web/locales/vi.yml +83 -0
  84. data/web/views/_footer.erb +5 -2
  85. data/web/views/_job_info.erb +2 -1
  86. data/web/views/_nav.erb +4 -18
  87. data/web/views/_paging.erb +1 -1
  88. data/web/views/busy.erb +15 -8
  89. data/web/views/dashboard.erb +1 -1
  90. data/web/views/dead.erb +2 -2
  91. data/web/views/layout.erb +12 -2
  92. data/web/views/morgue.erb +9 -6
  93. data/web/views/queue.erb +18 -8
  94. data/web/views/queues.erb +11 -1
  95. data/web/views/retries.erb +14 -7
  96. data/web/views/retry.erb +2 -2
  97. data/web/views/scheduled.erb +7 -4
  98. metadata +41 -188
  99. data/.github/issue_template.md +0 -9
  100. data/.travis.yml +0 -18
  101. data/bin/sidekiqctl +0 -99
  102. data/lib/sidekiq/core_ext.rb +0 -119
  103. data/lib/sidekiq/logging.rb +0 -106
  104. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  105. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  106. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sidekiq/monitor'
4
+
5
+ section = "all"
6
+ section = ARGV[0] if ARGV.size == 1
7
+
8
+ Sidekiq::Monitor::Status.new.display(section)
@@ -1,6 +1,6 @@
1
1
  require 'rails_helper'
2
2
  <% module_namespacing do -%>
3
3
  RSpec.describe <%= class_name %>Worker, type: :worker do
4
- pending "add some examples to (or delete) #{__FILE__}"
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
5
  end
6
6
  <% end -%>
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
  <% module_namespacing do -%>
3
- class <%= class_name %>WorkerTest < <% if defined? Minitest::Test %>Minitest::Test<% else %>MiniTest::Unit::TestCase<% end %>
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 'rails/generators/named_base'
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 'This generator creates a Sidekiq Worker in app/workers and a corresponding test'
6
+ desc "This generator creates a Sidekiq Worker in app/workers and a corresponding test"
7
7
 
8
- check_class_collision suffix: 'Worker'
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 'worker.rb.erb', File.join('app/workers', class_path, "#{file_name}_worker.rb")
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
- if defined?(RSpec)
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
- 'spec/workers',
31
- class_path,
32
- "#{file_name}_worker_spec.rb"
32
+ "spec/workers",
33
+ class_path,
34
+ "#{file_name}_worker_spec.rb"
33
35
  )
34
- template 'worker_spec.rb.erb', template_file
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
- 'test/workers',
40
- class_path,
41
- "#{file_name}_worker_test.rb"
41
+ "test/workers",
42
+ class_path,
43
+ "#{file_name}_worker_test.rb"
42
44
  )
43
- template 'worker_test.rb.erb', template_file
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
@@ -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 'sidekiq/logging'
7
- require 'sidekiq/client'
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 'json'
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 = 'Sidekiq'
15
- LICENSE = 'See LICENSE and the LGPL-3.0 for licensing details.'
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: 25,
21
- require: '.',
21
+ concurrency: 10,
22
+ require: ".",
23
+ strict: true,
22
24
  environment: nil,
23
- timeout: 8,
25
+ timeout: 25,
24
26
  poll_interval_average: nil,
25
- average_scheduled_poll_interval: 15,
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
- 'retry' => true,
41
- 'queue' => 'default'
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
- }.freeze
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::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/
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
- 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
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
- 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
154
+ Middleware::Chain.new
155
155
  end
156
156
 
157
157
  def self.default_worker_options=(hash)
158
- @default_worker_options = default_worker_options.merge(hash.stringify_keys)
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.default_retries_exhausted = -> (job, ex) do
172
+ # config.death_handlers << ->(job, ex) do
166
173
  # end
167
174
  # 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
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::Logging.logger
201
+ @logger ||= Sidekiq::Logger.new($stdout, level: Logger::INFO)
185
202
  end
186
- def self.logger=(log)
187
- Sidekiq::Logging.logger = log
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
- self.options[:average_scheduled_poll_interval] = interval
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
- self.options[:error_handlers]
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 'sidekiq/extensions/class_methods'
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)
@@ -1,6 +1,9 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
- require 'sidekiq'
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 do |conn|
55
+ pipe1_res = Sidekiq.redis { |conn|
53
56
  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)
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
- end
65
+ }
65
66
 
66
- pipe2_res = Sidekiq.redis do |conn|
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
- pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
69
- pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
77
+ processes.each { |key| conn.hget(key, "busy") }
78
+ queues.each { |queue| conn.llen("queue:#{queue}") }
70
79
  end
71
- end
80
+ }
72
81
 
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, &:+)
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
- 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
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: 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],
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: workers_size,
95
- enqueued: enqueued
107
+ workers_size: workers_size,
108
+ enqueued: enqueued
96
109
  }
97
110
  end
98
111
 
99
112
  def reset(*stats)
100
- all = %w(failed processed)
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.smembers('queues'.freeze)
135
+ queues = conn.sscan_each("queues").to_a
123
136
 
124
- lengths = conn.pipelined do
137
+ lengths = conn.pipelined {
125
138
  queues.each do |queue|
126
139
  conn.llen("queue:#{queue}")
127
140
  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 }
141
+ }
136
142
 
137
- Hash[array_of_arrays.reverse]
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
- 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
167
+ dates = @start_date.downto(@start_date - @days_previous + 1).map { |date|
168
+ date.strftime("%Y-%m-%d")
169
+ }
171
170
 
172
- Sidekiq.redis do |conn|
173
- conn.mget(keys).each_with_index do |value, idx|
174
- stat_hash[dates[idx]] = value ? value.to_i : 0
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.smembers('queues'.freeze) }.sort.map { |q| Sidekiq::Queue.new(q) }
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 do |conn|
233
+ entry = Sidekiq.redis { |conn|
228
234
  conn.lrange(@rname, -1, -1)
229
- end.first
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['enqueued_at'] || now
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
- while true do
249
+ loop do
244
250
  range_start = page * page_size - deleted_size
245
- range_end = range_start + page_size - 1
246
- entries = Sidekiq.redis do |conn|
251
+ range_end = range_start + page_size - 1
252
+ entries = Sidekiq.redis { |conn|
247
253
  conn.lrange @rname, range_start, range_end
248
- end
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.del(@rname)
271
- conn.srem("queues".freeze, name)
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 : Sidekiq.load_json(item)
292
- @queue = queue_name || @item['queue']
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
- @item['class']
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['wrapped'] || args[0]
308
- if 'ActionMailer::DeliveryJob' == job_class
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]['arguments'][0..1].join('#')
328
+ args[0]["arguments"][0..1].join("#")
311
329
  else
312
- job_class
330
+ job_class
313
331
  end
314
332
  else
315
333
  klass
316
- end
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
- @args ||= case klass
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 = @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)
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
- job_args
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
- end
361
+ end
337
362
  end
338
363
 
339
364
  def args
340
- @item['args']
365
+ @args || @item["args"]
341
366
  end
342
367
 
343
368
  def jid
344
- @item['jid']
369
+ self["jid"]
345
370
  end
346
371
 
347
372
  def enqueued_at
348
- @item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
373
+ self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
349
374
  end
350
375
 
351
376
  def created_at
352
- Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
377
+ Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
353
378
  end
354
379
 
355
- def queue
356
- @queue
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['enqueued_at'] || @item['created_at'] || now)
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 do |conn|
404
+ count = Sidekiq.redis { |conn|
368
405
  conn.lrem("queue:#{@queue}", 1, @value)
369
- end
406
+ }
370
407
  count != 0
371
408
  end
372
409
 
373
410
  def [](name)
374
- @item[name]
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
- 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
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
- delete
415
- @parent.schedule(at, item)
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['retry_count'] -= 1 if msg['retry_count']
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
- 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
493
+ DeadSet.new.kill(message)
445
494
  end
446
495
  end
447
496
 
448
497
  def error?
449
- !!item['error_class']
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 do
505
+ results = conn.multi {
457
506
  conn.zrangebyscore(parent.name, score, score)
458
507
  conn.zremrangebyscore(parent.name, score, score)
459
- end.first
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
- hash = results.group_by do |message|
515
+ matched, nonmatched = results.partition { |message|
467
516
  if message.index(jid)
468
517
  msg = Sidekiq.load_json(message)
469
- msg['jid'] == jid
518
+ msg["jid"] == jid
470
519
  else
471
520
  false
472
521
  end
473
- end
522
+ }
474
523
 
475
- msg = hash.fetch(true, []).first
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
- hash.fetch(false, []).each do |message|
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.del(name)
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
- while true do
584
+ loop do
527
585
  range_start = page * page_size + offset_size
528
- range_end = range_start + page_size - 1
529
- elements = Sidekiq.redis do |conn|
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
- end
589
+ }
532
590
  break if elements.empty?
533
591
  page -= 1
534
- elements.each do |element, score|
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
- elements = Sidekiq.redis do |conn|
543
- conn.zrangebyscore(name, score, score)
544
- end
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
- result << entry
607
+ [score, score]
552
608
  end
553
- result
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
- self.detect { |j| j.jid == jid }
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
- message = Sidekiq.load_json(element)
579
- if message["jid"] == jid
580
- ret = conn.zrem(name, element)
581
- @_size -= 1 if ret
582
- break ret
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 'schedule'
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 'retry'
694
+ super "retry"
625
695
  end
626
696
 
627
697
  def retry_all
628
- while size > 0
629
- each(&:retry)
630
- end
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 'dead'
711
+ super "dead"
640
712
  end
641
713
 
642
- def retry_all
643
- while size > 0
644
- each(&:retry)
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 send a heartbeat to Redis every 5 seconds
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
- self.class.cleanup if clean_plz
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 self.cleanup
764
+ def cleanup
674
765
  count = 0
675
766
  Sidekiq.redis do |conn|
676
- procs = conn.smembers('processes').sort
677
- heartbeats = conn.pipelined do
767
+ procs = conn.sscan_each("processes").to_a.sort
768
+ heartbeats = conn.pipelined {
678
769
  procs.each do |key|
679
- conn.hget(key, 'info')
770
+ conn.hget(key, "info")
680
771
  end
681
- end
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
- 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?
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
- procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
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
- result = conn.pipelined do
792
+ conn.pipelined do
703
793
  procs.each do |key|
704
- conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
794
+ conn.hmget(key, "info", "busy", "beat", "quiet")
705
795
  end
706
796
  end
797
+ }
707
798
 
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
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
- nil
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('processes') }
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['tag']
852
+ self["tag"]
748
853
  end
749
854
 
750
855
  def labels
751
- Array(self['labels'])
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('USR1')
868
+ signal("TSTP")
760
869
  end
761
870
 
762
871
  def stop!
763
- signal('TERM')
872
+ signal("TERM")
764
873
  end
765
874
 
766
875
  def dump_threads
767
- signal('TTIN')
876
+ signal("TTIN")
768
877
  end
769
878
 
770
879
  def stopping?
771
- self['quiet'] == 'true'
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.smembers('processes')
922
+ procs = conn.sscan_each("processes").to_a
817
923
  procs.sort.each do |key|
818
- valid, workers = conn.pipelined do
819
- conn.exists(key)
924
+ valid, workers = conn.pipelined {
925
+ conn.exists?(key)
820
926
  conn.hgetall("#{key}:workers")
821
- end
927
+ }
822
928
  next unless valid
823
929
  workers.each_pair do |tid, json|
824
- yield key, tid, Sidekiq.load_json(json)
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.smembers('processes')
950
+ procs = conn.sscan_each("processes").to_a
839
951
  if procs.empty?
840
952
  0
841
953
  else
842
- conn.pipelined do
954
+ conn.pipelined {
843
955
  procs.each do |key|
844
- conn.hget(key, 'busy')
956
+ conn.hget(key, "busy")
845
957
  end
846
- end.map(&:to_i).inject(:+)
958
+ }.sum(&:to_i)
847
959
  end
848
960
  end
849
961
  end
850
962
  end
851
-
852
963
  end