sidekiq 4.2.2 → 6.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +516 -0
  3. data/LICENSE +2 -2
  4. data/README.md +23 -36
  5. data/bin/sidekiq +26 -2
  6. data/bin/sidekiqload +28 -38
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  9. data/lib/generators/sidekiq/templates/worker_test.rb.erb +2 -2
  10. data/lib/generators/sidekiq/worker_generator.rb +21 -13
  11. data/lib/sidekiq/api.rb +401 -243
  12. data/lib/sidekiq/cli.rb +228 -212
  13. data/lib/sidekiq/client.rb +76 -53
  14. data/lib/sidekiq/delay.rb +41 -0
  15. data/lib/sidekiq/exception_handler.rb +12 -16
  16. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  17. data/lib/sidekiq/extensions/active_record.rb +13 -10
  18. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  19. data/lib/sidekiq/extensions/generic_proxy.rb +12 -4
  20. data/lib/sidekiq/fetch.rb +39 -31
  21. data/lib/sidekiq/job.rb +13 -0
  22. data/lib/sidekiq/job_logger.rb +63 -0
  23. data/lib/sidekiq/job_retry.rb +259 -0
  24. data/lib/sidekiq/launcher.rb +170 -71
  25. data/lib/sidekiq/logger.rb +166 -0
  26. data/lib/sidekiq/manager.rb +17 -20
  27. data/lib/sidekiq/middleware/chain.rb +20 -8
  28. data/lib/sidekiq/middleware/current_attributes.rb +52 -0
  29. data/lib/sidekiq/middleware/i18n.rb +5 -7
  30. data/lib/sidekiq/monitor.rb +133 -0
  31. data/lib/sidekiq/paginator.rb +18 -14
  32. data/lib/sidekiq/processor.rb +169 -78
  33. data/lib/sidekiq/rails.rb +41 -36
  34. data/lib/sidekiq/redis_connection.rb +65 -20
  35. data/lib/sidekiq/scheduled.rb +85 -34
  36. data/lib/sidekiq/sd_notify.rb +149 -0
  37. data/lib/sidekiq/systemd.rb +24 -0
  38. data/lib/sidekiq/testing/inline.rb +2 -1
  39. data/lib/sidekiq/testing.rb +52 -26
  40. data/lib/sidekiq/util.rb +48 -15
  41. data/lib/sidekiq/version.rb +2 -1
  42. data/lib/sidekiq/web/action.rb +15 -17
  43. data/lib/sidekiq/web/application.rb +114 -92
  44. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  45. data/lib/sidekiq/web/helpers.rb +151 -83
  46. data/lib/sidekiq/web/router.rb +27 -19
  47. data/lib/sidekiq/web.rb +85 -76
  48. data/lib/sidekiq/worker.rb +233 -43
  49. data/lib/sidekiq.rb +88 -64
  50. data/sidekiq.gemspec +24 -22
  51. data/web/assets/images/apple-touch-icon.png +0 -0
  52. data/web/assets/javascripts/application.js +86 -59
  53. data/web/assets/javascripts/dashboard.js +81 -85
  54. data/web/assets/stylesheets/application-dark.css +147 -0
  55. data/web/assets/stylesheets/application-rtl.css +242 -0
  56. data/web/assets/stylesheets/application.css +319 -141
  57. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  58. data/web/assets/stylesheets/bootstrap.css +2 -2
  59. data/web/locales/ar.yml +87 -0
  60. data/web/locales/de.yml +14 -2
  61. data/web/locales/en.yml +8 -1
  62. data/web/locales/es.yml +22 -5
  63. data/web/locales/fa.yml +80 -0
  64. data/web/locales/fr.yml +10 -3
  65. data/web/locales/he.yml +79 -0
  66. data/web/locales/ja.yml +12 -4
  67. data/web/locales/lt.yml +83 -0
  68. data/web/locales/pl.yml +4 -4
  69. data/web/locales/ru.yml +4 -0
  70. data/web/locales/ur.yml +80 -0
  71. data/web/locales/vi.yml +83 -0
  72. data/web/views/_footer.erb +5 -2
  73. data/web/views/_job_info.erb +4 -3
  74. data/web/views/_nav.erb +4 -18
  75. data/web/views/_paging.erb +1 -1
  76. data/web/views/_poll_link.erb +2 -5
  77. data/web/views/_summary.erb +7 -7
  78. data/web/views/busy.erb +60 -22
  79. data/web/views/dashboard.erb +23 -15
  80. data/web/views/dead.erb +3 -3
  81. data/web/views/layout.erb +14 -3
  82. data/web/views/morgue.erb +19 -12
  83. data/web/views/queue.erb +24 -14
  84. data/web/views/queues.erb +14 -4
  85. data/web/views/retries.erb +22 -13
  86. data/web/views/retry.erb +4 -4
  87. data/web/views/scheduled.erb +7 -4
  88. metadata +44 -194
  89. data/.github/contributing.md +0 -32
  90. data/.github/issue_template.md +0 -4
  91. data/.gitignore +0 -12
  92. data/.travis.yml +0 -12
  93. data/3.0-Upgrade.md +0 -70
  94. data/4.0-Upgrade.md +0 -53
  95. data/COMM-LICENSE +0 -95
  96. data/Ent-Changes.md +0 -146
  97. data/Gemfile +0 -29
  98. data/Pro-2.0-Upgrade.md +0 -138
  99. data/Pro-3.0-Upgrade.md +0 -44
  100. data/Pro-Changes.md +0 -570
  101. data/Rakefile +0 -9
  102. data/bin/sidekiqctl +0 -99
  103. data/code_of_conduct.md +0 -50
  104. data/lib/sidekiq/core_ext.rb +0 -106
  105. data/lib/sidekiq/logging.rb +0 -106
  106. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  107. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  108. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  109. data/test/config.yml +0 -9
  110. data/test/env_based_config.yml +0 -11
  111. data/test/fake_env.rb +0 -1
  112. data/test/fixtures/en.yml +0 -2
  113. data/test/helper.rb +0 -75
  114. data/test/test_actors.rb +0 -138
  115. data/test/test_api.rb +0 -528
  116. data/test/test_cli.rb +0 -418
  117. data/test/test_client.rb +0 -266
  118. data/test/test_exception_handler.rb +0 -56
  119. data/test/test_extensions.rb +0 -127
  120. data/test/test_fetch.rb +0 -50
  121. data/test/test_launcher.rb +0 -95
  122. data/test/test_logging.rb +0 -35
  123. data/test/test_manager.rb +0 -50
  124. data/test/test_middleware.rb +0 -158
  125. data/test/test_processor.rb +0 -201
  126. data/test/test_rails.rb +0 -22
  127. data/test/test_redis_connection.rb +0 -132
  128. data/test/test_retry.rb +0 -326
  129. data/test/test_retry_exhausted.rb +0 -149
  130. data/test/test_scheduled.rb +0 -115
  131. data/test/test_scheduling.rb +0 -50
  132. data/test/test_sidekiq.rb +0 -107
  133. data/test/test_testing.rb +0 -143
  134. data/test/test_testing_fake.rb +0 -357
  135. data/test/test_testing_inline.rb +0 -94
  136. data/test/test_util.rb +0 -13
  137. data/test/test_web.rb +0 -666
  138. data/test/test_web_helpers.rb +0 -54
data/lib/sidekiq/api.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
- # encoding: utf-8
3
- require 'sidekiq'
2
+
3
+ require "sidekiq"
4
+
5
+ require "zlib"
6
+ require "base64"
4
7
 
5
8
  module Sidekiq
6
9
  class Stats
7
10
  def initialize
8
- fetch_stats!
11
+ fetch_stats_fast!
9
12
  end
10
13
 
11
14
  def processed
@@ -48,53 +51,78 @@ module Sidekiq
48
51
  Sidekiq::Stats::Queues.new.lengths
49
52
  end
50
53
 
51
- def fetch_stats!
52
- pipe1_res = Sidekiq.redis do |conn|
54
+ # O(1) redis calls
55
+ def fetch_stats_fast!
56
+ pipe1_res = Sidekiq.redis { |conn|
53
57
  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)
58
+ conn.get("stat:processed")
59
+ conn.get("stat:failed")
60
+ conn.zcard("schedule")
61
+ conn.zcard("retry")
62
+ conn.zcard("dead")
63
+ conn.scard("processes")
64
+ conn.lrange("queue:default", -1, -1)
63
65
  end
64
- end
66
+ }
65
67
 
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}") }
68
+ default_queue_latency = if (entry = pipe1_res[6].first)
69
+ job = begin
70
+ Sidekiq.load_json(entry)
71
+ rescue
72
+ {}
70
73
  end
74
+ now = Time.now.to_f
75
+ thence = job["enqueued_at"] || now
76
+ now - thence
77
+ else
78
+ 0
71
79
  end
72
80
 
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
- Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at'.freeze]
79
- else
80
- 0
81
- end
82
81
  @stats = {
83
- processed: pipe1_res[0].to_i,
84
- failed: pipe1_res[1].to_i,
85
- scheduled_size: pipe1_res[2],
86
- retry_size: pipe1_res[3],
87
- dead_size: pipe1_res[4],
88
- processes_size: pipe1_res[5],
89
-
90
- default_queue_latency: default_queue_latency,
91
- workers_size: workers_size,
92
- enqueued: enqueued
82
+ processed: pipe1_res[0].to_i,
83
+ failed: pipe1_res[1].to_i,
84
+ scheduled_size: pipe1_res[2],
85
+ retry_size: pipe1_res[3],
86
+ dead_size: pipe1_res[4],
87
+ processes_size: pipe1_res[5],
88
+
89
+ default_queue_latency: default_queue_latency
93
90
  }
94
91
  end
95
92
 
93
+ # O(number of processes + number of queues) redis calls
94
+ def fetch_stats_slow!
95
+ processes = Sidekiq.redis { |conn|
96
+ conn.sscan_each("processes").to_a
97
+ }
98
+
99
+ queues = Sidekiq.redis { |conn|
100
+ conn.sscan_each("queues").to_a
101
+ }
102
+
103
+ pipe2_res = Sidekiq.redis { |conn|
104
+ conn.pipelined do
105
+ processes.each { |key| conn.hget(key, "busy") }
106
+ queues.each { |queue| conn.llen("queue:#{queue}") }
107
+ end
108
+ }
109
+
110
+ s = processes.size
111
+ workers_size = pipe2_res[0...s].sum(&:to_i)
112
+ enqueued = pipe2_res[s..-1].sum(&:to_i)
113
+
114
+ @stats[:workers_size] = workers_size
115
+ @stats[:enqueued] = enqueued
116
+ @stats
117
+ end
118
+
119
+ def fetch_stats!
120
+ fetch_stats_fast!
121
+ fetch_stats_slow!
122
+ end
123
+
96
124
  def reset(*stats)
97
- all = %w(failed processed)
125
+ all = %w[failed processed]
98
126
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
99
127
 
100
128
  mset_args = []
@@ -110,28 +138,23 @@ module Sidekiq
110
138
  private
111
139
 
112
140
  def stat(s)
113
- @stats[s]
141
+ fetch_stats_slow! if @stats[s].nil?
142
+ @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
114
143
  end
115
144
 
116
145
  class Queues
117
146
  def lengths
118
147
  Sidekiq.redis do |conn|
119
- queues = conn.smembers('queues'.freeze)
148
+ queues = conn.sscan_each("queues").to_a
120
149
 
121
- lengths = conn.pipelined do
150
+ lengths = conn.pipelined {
122
151
  queues.each do |queue|
123
152
  conn.llen("queue:#{queue}")
124
153
  end
125
- end
126
-
127
- i = 0
128
- array_of_arrays = queues.inject({}) do |memo, queue|
129
- memo[queue] = lengths[i]
130
- i += 1
131
- memo
132
- end.sort_by { |_, size| size }
154
+ }
133
155
 
134
- Hash[array_of_arrays.reverse]
156
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
157
+ array_of_arrays.to_h
135
158
  end
136
159
  end
137
160
  end
@@ -143,33 +166,32 @@ module Sidekiq
143
166
  end
144
167
 
145
168
  def processed
146
- date_stat_hash("processed")
169
+ @processed ||= date_stat_hash("processed")
147
170
  end
148
171
 
149
172
  def failed
150
- date_stat_hash("failed")
173
+ @failed ||= date_stat_hash("failed")
151
174
  end
152
175
 
153
176
  private
154
177
 
155
178
  def date_stat_hash(stat)
156
- i = 0
157
179
  stat_hash = {}
158
- keys = []
159
- dates = []
160
-
161
- while i < @days_previous
162
- date = @start_date - i
163
- datestr = date.strftime("%Y-%m-%d".freeze)
164
- keys << "stat:#{stat}:#{datestr}"
165
- dates << datestr
166
- i += 1
167
- end
180
+ dates = @start_date.downto(@start_date - @days_previous + 1).map { |date|
181
+ date.strftime("%Y-%m-%d")
182
+ }
168
183
 
169
- Sidekiq.redis do |conn|
170
- conn.mget(keys).each_with_index do |value, idx|
171
- stat_hash[dates[idx]] = value ? value.to_i : 0
184
+ keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
185
+
186
+ begin
187
+ Sidekiq.redis do |conn|
188
+ conn.mget(keys).each_with_index do |value, idx|
189
+ stat_hash[dates[idx]] = value ? value.to_i : 0
190
+ end
172
191
  end
192
+ rescue Redis::CommandError
193
+ # mget will trigger a CROSSSLOT error when run against a Cluster
194
+ # TODO Someone want to add Cluster support?
173
195
  end
174
196
 
175
197
  stat_hash
@@ -196,13 +218,13 @@ module Sidekiq
196
218
  # Return all known queues within Redis.
197
219
  #
198
220
  def self.all
199
- Sidekiq.redis { |c| c.smembers('queues'.freeze) }.sort.map { |q| Sidekiq::Queue.new(q) }
221
+ Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
200
222
  end
201
223
 
202
224
  attr_reader :name
203
225
 
204
- def initialize(name="default")
205
- @name = name
226
+ def initialize(name = "default")
227
+ @name = name.to_s
206
228
  @rname = "queue:#{name}"
207
229
  end
208
230
 
@@ -221,11 +243,14 @@ module Sidekiq
221
243
  #
222
244
  # @return Float
223
245
  def latency
224
- entry = Sidekiq.redis do |conn|
246
+ entry = Sidekiq.redis { |conn|
225
247
  conn.lrange(@rname, -1, -1)
226
- end.first
248
+ }.first
227
249
  return 0 unless entry
228
- Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at']
250
+ job = Sidekiq.load_json(entry)
251
+ now = Time.now.to_f
252
+ thence = job["enqueued_at"] || now
253
+ now - thence
229
254
  end
230
255
 
231
256
  def each
@@ -234,16 +259,16 @@ module Sidekiq
234
259
  page = 0
235
260
  page_size = 50
236
261
 
237
- while true do
262
+ loop do
238
263
  range_start = page * page_size - deleted_size
239
- range_end = range_start + page_size - 1
240
- entries = Sidekiq.redis do |conn|
264
+ range_end = range_start + page_size - 1
265
+ entries = Sidekiq.redis { |conn|
241
266
  conn.lrange @rname, range_start, range_end
242
- end
267
+ }
243
268
  break if entries.empty?
244
269
  page += 1
245
270
  entries.each do |entry|
246
- yield Job.new(entry, @name)
271
+ yield JobRecord.new(entry, @name)
247
272
  end
248
273
  deleted_size = initial_size - size
249
274
  end
@@ -253,7 +278,7 @@ module Sidekiq
253
278
  # Find the job with the given JID within this queue.
254
279
  #
255
280
  # This is a slow, inefficient operation. Do not use under
256
- # normal conditions. Sidekiq Pro contains a faster version.
281
+ # normal conditions.
257
282
  def find_job(jid)
258
283
  detect { |j| j.jid == jid }
259
284
  end
@@ -261,8 +286,8 @@ module Sidekiq
261
286
  def clear
262
287
  Sidekiq.redis do |conn|
263
288
  conn.multi do
264
- conn.del(@rname)
265
- conn.srem("queues".freeze, name)
289
+ conn.unlink(@rname)
290
+ conn.srem("queues", name)
266
291
  end
267
292
  end
268
293
  end
@@ -274,114 +299,166 @@ module Sidekiq
274
299
  # sorted set.
275
300
  #
276
301
  # The job should be considered immutable but may be
277
- # removed from the queue via Job#delete.
302
+ # removed from the queue via JobRecord#delete.
278
303
  #
279
- class Job
304
+ class JobRecord
280
305
  attr_reader :item
281
306
  attr_reader :value
282
307
 
283
- def initialize(item, queue_name=nil)
308
+ def initialize(item, queue_name = nil)
309
+ @args = nil
284
310
  @value = item
285
- @item = item.is_a?(Hash) ? item : Sidekiq.load_json(item)
286
- @queue = queue_name || @item['queue']
311
+ @item = item.is_a?(Hash) ? item : parse(item)
312
+ @queue = queue_name || @item["queue"]
313
+ end
314
+
315
+ def parse(item)
316
+ Sidekiq.load_json(item)
317
+ rescue JSON::ParserError
318
+ # If the job payload in Redis is invalid JSON, we'll load
319
+ # the item as an empty hash and store the invalid JSON as
320
+ # the job 'args' for display in the Web UI.
321
+ @invalid = true
322
+ @args = [item]
323
+ {}
287
324
  end
288
325
 
289
326
  def klass
290
- @item['class']
327
+ self["class"]
291
328
  end
292
329
 
293
330
  def display_class
294
331
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
295
- @klass ||= case klass
296
- when /\ASidekiq::Extensions::Delayed/
297
- safe_load(args[0], klass) do |target, method, _|
298
- "#{target}.#{method}"
299
- end
300
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
301
- job_class = @item['wrapped'] || args[0]
302
- if 'ActionMailer::DeliveryJob' == job_class
303
- # MailerClass#mailer_method
304
- args[0]['arguments'][0..1].join('#')
305
- else
306
- job_class
307
- end
308
- else
309
- klass
310
- end
332
+ @klass ||= self["display_class"] || begin
333
+ case klass
334
+ when /\ASidekiq::Extensions::Delayed/
335
+ safe_load(args[0], klass) do |target, method, _|
336
+ "#{target}.#{method}"
337
+ end
338
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
339
+ job_class = @item["wrapped"] || args[0]
340
+ if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
341
+ # MailerClass#mailer_method
342
+ args[0]["arguments"][0..1].join("#")
343
+ else
344
+ job_class
345
+ end
346
+ else
347
+ klass
348
+ end
349
+ end
311
350
  end
312
351
 
313
352
  def display_args
314
353
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
315
- @args ||= case klass
354
+ @display_args ||= case klass
316
355
  when /\ASidekiq::Extensions::Delayed/
317
356
  safe_load(args[0], args) do |_, _, arg|
318
357
  arg
319
358
  end
320
359
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
321
- job_args = @item['wrapped'] ? args[0]["arguments"] : []
322
- if 'ActionMailer::DeliveryJob' == (@item['wrapped'] || args[0])
323
- # remove MailerClass, mailer_method and 'deliver_now'
324
- job_args.drop(3)
360
+ job_args = self["wrapped"] ? args[0]["arguments"] : []
361
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
362
+ # remove MailerClass, mailer_method and 'deliver_now'
363
+ job_args.drop(3)
364
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
365
+ # remove MailerClass, mailer_method and 'deliver_now'
366
+ job_args.drop(3).first["args"]
325
367
  else
326
- job_args
368
+ job_args
327
369
  end
328
370
  else
371
+ if self["encrypt"]
372
+ # no point in showing 150+ bytes of random garbage
373
+ args[-1] = "[encrypted data]"
374
+ end
329
375
  args
330
- end
376
+ end
331
377
  end
332
378
 
333
379
  def args
334
- @item['args']
380
+ @args || @item["args"]
335
381
  end
336
382
 
337
383
  def jid
338
- @item['jid']
384
+ self["jid"]
339
385
  end
340
386
 
341
387
  def enqueued_at
342
- @item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
388
+ self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
343
389
  end
344
390
 
345
391
  def created_at
346
- Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
392
+ Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
393
+ end
394
+
395
+ def tags
396
+ self["tags"] || []
347
397
  end
348
398
 
349
- def queue
350
- @queue
399
+ def error_backtrace
400
+ # Cache nil values
401
+ if defined?(@error_backtrace)
402
+ @error_backtrace
403
+ else
404
+ value = self["error_backtrace"]
405
+ @error_backtrace = value && uncompress_backtrace(value)
406
+ end
351
407
  end
352
408
 
409
+ attr_reader :queue
410
+
353
411
  def latency
354
- Time.now.to_f - (@item['enqueued_at'] || @item['created_at'])
412
+ now = Time.now.to_f
413
+ now - (@item["enqueued_at"] || @item["created_at"] || now)
355
414
  end
356
415
 
357
416
  ##
358
417
  # Remove this job from the queue.
359
418
  def delete
360
- count = Sidekiq.redis do |conn|
419
+ count = Sidekiq.redis { |conn|
361
420
  conn.lrem("queue:#{@queue}", 1, @value)
362
- end
421
+ }
363
422
  count != 0
364
423
  end
365
424
 
366
425
  def [](name)
367
- @item[name]
426
+ # nil will happen if the JSON fails to parse.
427
+ # We don't guarantee Sidekiq will work with bad job JSON but we should
428
+ # make a best effort to minimize the damage.
429
+ @item ? @item[name] : nil
368
430
  end
369
431
 
370
432
  private
371
433
 
372
434
  def safe_load(content, default)
373
- begin
374
- yield(*YAML.load(content))
375
- rescue => ex
376
- # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
377
- # memory yet so the YAML can't be loaded.
378
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == 'development'
379
- default
435
+ yield(*YAML.load(content))
436
+ rescue => ex
437
+ # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
438
+ # memory yet so the YAML can't be loaded.
439
+ Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
440
+ default
441
+ end
442
+
443
+ def uncompress_backtrace(backtrace)
444
+ if backtrace.is_a?(Array)
445
+ # Handle old jobs with raw Array backtrace format
446
+ backtrace
447
+ else
448
+ decoded = Base64.decode64(backtrace)
449
+ uncompressed = Zlib::Inflate.inflate(decoded)
450
+ begin
451
+ Sidekiq.load_json(uncompressed)
452
+ rescue
453
+ # Handle old jobs with marshalled backtrace format
454
+ # TODO Remove in 7.x
455
+ Marshal.load(uncompressed)
456
+ end
380
457
  end
381
458
  end
382
459
  end
383
460
 
384
- class SortedEntry < Job
461
+ class SortedEntry < JobRecord
385
462
  attr_reader :score
386
463
  attr_reader :parent
387
464
 
@@ -404,8 +481,9 @@ module Sidekiq
404
481
  end
405
482
 
406
483
  def reschedule(at)
407
- delete
408
- @parent.schedule(at, item)
484
+ Sidekiq.redis do |conn|
485
+ conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
486
+ end
409
487
  end
410
488
 
411
489
  def add_to_queue
@@ -416,10 +494,9 @@ module Sidekiq
416
494
  end
417
495
 
418
496
  def retry
419
- raise "Retry not available on jobs which have not failed" unless item["failed_at"]
420
497
  remove_job do |message|
421
498
  msg = Sidekiq.load_json(message)
422
- msg['retry_count'] -= 1
499
+ msg["retry_count"] -= 1 if msg["retry_count"]
423
500
  Sidekiq::Client.push(msg)
424
501
  end
425
502
  end
@@ -427,56 +504,50 @@ module Sidekiq
427
504
  ##
428
505
  # Place job in the dead set
429
506
  def kill
430
- raise 'Kill not available on jobs which have not failed' unless item['failed_at']
431
507
  remove_job do |message|
432
- Sidekiq.logger.info { "Killing job #{message['jid']}" }
433
- now = Time.now.to_f
434
- Sidekiq.redis do |conn|
435
- conn.multi do
436
- conn.zadd('dead', now, message)
437
- conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
438
- conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
439
- end
440
- end
508
+ DeadSet.new.kill(message)
441
509
  end
442
510
  end
443
511
 
512
+ def error?
513
+ !!item["error_class"]
514
+ end
515
+
444
516
  private
445
517
 
446
518
  def remove_job
447
519
  Sidekiq.redis do |conn|
448
- results = conn.multi do
520
+ results = conn.multi {
449
521
  conn.zrangebyscore(parent.name, score, score)
450
522
  conn.zremrangebyscore(parent.name, score, score)
451
- end.first
523
+ }.first
452
524
 
453
525
  if results.size == 1
454
526
  yield results.first
455
527
  else
456
528
  # multiple jobs with the same score
457
529
  # find the one with the right JID and push it
458
- hash = results.group_by do |message|
530
+ matched, nonmatched = results.partition { |message|
459
531
  if message.index(jid)
460
532
  msg = Sidekiq.load_json(message)
461
- msg['jid'] == jid
533
+ msg["jid"] == jid
462
534
  else
463
535
  false
464
536
  end
465
- end
537
+ }
466
538
 
467
- msg = hash.fetch(true, []).first
539
+ msg = matched.first
468
540
  yield msg if msg
469
541
 
470
542
  # push the rest back onto the sorted set
471
543
  conn.multi do
472
- hash.fetch(false, []).each do |message|
544
+ nonmatched.each do |message|
473
545
  conn.zadd(parent.name, score.to_f.to_s, message)
474
546
  end
475
547
  end
476
548
  end
477
549
  end
478
550
  end
479
-
480
551
  end
481
552
 
482
553
  class SortedSet
@@ -493,16 +564,26 @@ module Sidekiq
493
564
  Sidekiq.redis { |c| c.zcard(name) }
494
565
  end
495
566
 
567
+ def scan(match, count = 100)
568
+ return to_enum(:scan, match, count) unless block_given?
569
+
570
+ match = "*#{match}*" unless match.include?("*")
571
+ Sidekiq.redis do |conn|
572
+ conn.zscan_each(name, match: match, count: count) do |entry, score|
573
+ yield SortedEntry.new(self, score, entry)
574
+ end
575
+ end
576
+ end
577
+
496
578
  def clear
497
579
  Sidekiq.redis do |conn|
498
- conn.del(name)
580
+ conn.unlink(name)
499
581
  end
500
582
  end
501
583
  alias_method :💣, :clear
502
584
  end
503
585
 
504
586
  class JobSet < SortedSet
505
-
506
587
  def schedule(timestamp, message)
507
588
  Sidekiq.redis do |conn|
508
589
  conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
@@ -515,44 +596,55 @@ module Sidekiq
515
596
  page = -1
516
597
  page_size = 50
517
598
 
518
- while true do
599
+ loop do
519
600
  range_start = page * page_size + offset_size
520
- range_end = range_start + page_size - 1
521
- elements = Sidekiq.redis do |conn|
601
+ range_end = range_start + page_size - 1
602
+ elements = Sidekiq.redis { |conn|
522
603
  conn.zrange name, range_start, range_end, with_scores: true
523
- end
604
+ }
524
605
  break if elements.empty?
525
606
  page -= 1
526
- elements.each do |element, score|
607
+ elements.reverse_each do |element, score|
527
608
  yield SortedEntry.new(self, score, element)
528
609
  end
529
610
  offset_size = initial_size - @_size
530
611
  end
531
612
  end
532
613
 
614
+ ##
615
+ # Fetch jobs that match a given time or Range. Job ID is an
616
+ # optional second argument.
533
617
  def fetch(score, jid = nil)
534
- elements = Sidekiq.redis do |conn|
535
- conn.zrangebyscore(name, score, score)
536
- end
537
-
538
- elements.inject([]) do |result, element|
539
- entry = SortedEntry.new(self, score, element)
540
- if jid
541
- result << entry if entry.jid == jid
618
+ begin_score, end_score =
619
+ if score.is_a?(Range)
620
+ [score.first, score.last]
542
621
  else
543
- result << entry
622
+ [score, score]
544
623
  end
545
- result
624
+
625
+ elements = Sidekiq.redis { |conn|
626
+ conn.zrangebyscore(name, begin_score, end_score, with_scores: true)
627
+ }
628
+
629
+ elements.each_with_object([]) do |element, result|
630
+ data, job_score = element
631
+ entry = SortedEntry.new(self, job_score, data)
632
+ result << entry if jid.nil? || entry.jid == jid
546
633
  end
547
634
  end
548
635
 
549
636
  ##
550
637
  # Find the job with the given JID within this sorted set.
551
- #
552
- # This is a slow, inefficient operation. Do not use under
553
- # normal conditions. Sidekiq Pro contains a faster version.
638
+ # This is a slower O(n) operation. Do not use for app logic.
554
639
  def find_job(jid)
555
- self.detect { |j| j.jid == jid }
640
+ Sidekiq.redis do |conn|
641
+ conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
642
+ job = JSON.parse(entry)
643
+ matched = job["jid"] == jid
644
+ return SortedEntry.new(self, score, entry) if matched
645
+ end
646
+ end
647
+ nil
556
648
  end
557
649
 
558
650
  def delete_by_value(name, value)
@@ -567,13 +659,14 @@ module Sidekiq
567
659
  Sidekiq.redis do |conn|
568
660
  elements = conn.zrangebyscore(name, score, score)
569
661
  elements.each do |element|
570
- message = Sidekiq.load_json(element)
571
- if message["jid"] == jid
572
- ret = conn.zrem(name, element)
573
- @_size -= 1 if ret
574
- break ret
662
+ if element.index(jid)
663
+ message = Sidekiq.load_json(element)
664
+ if message["jid"] == jid
665
+ ret = conn.zrem(name, element)
666
+ @_size -= 1 if ret
667
+ break ret
668
+ end
575
669
  end
576
- false
577
670
  end
578
671
  end
579
672
  end
@@ -585,17 +678,17 @@ module Sidekiq
585
678
  # Allows enumeration of scheduled jobs within Sidekiq.
586
679
  # Based on this, you can search/filter for jobs. Here's an
587
680
  # example where I'm selecting all jobs of a certain type
588
- # and deleting them from the retry queue.
681
+ # and deleting them from the schedule queue.
589
682
  #
590
683
  # r = Sidekiq::ScheduledSet.new
591
- # r.select do |retri|
592
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
593
- # retri.args[0] == 'User' &&
594
- # retri.args[1] == 'setup_new_subscriber'
684
+ # r.select do |scheduled|
685
+ # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
686
+ # scheduled.args[0] == 'User' &&
687
+ # scheduled.args[1] == 'setup_new_subscriber'
595
688
  # end.map(&:delete)
596
689
  class ScheduledSet < JobSet
597
690
  def initialize
598
- super 'schedule'
691
+ super "schedule"
599
692
  end
600
693
  end
601
694
 
@@ -613,13 +706,15 @@ module Sidekiq
613
706
  # end.map(&:delete)
614
707
  class RetrySet < JobSet
615
708
  def initialize
616
- super 'retry'
709
+ super "retry"
617
710
  end
618
711
 
619
712
  def retry_all
620
- while size > 0
621
- each(&:retry)
622
- end
713
+ each(&:retry) while size > 0
714
+ end
715
+
716
+ def kill_all
717
+ each(&:kill) while size > 0
623
718
  end
624
719
  end
625
720
 
@@ -628,13 +723,32 @@ module Sidekiq
628
723
  #
629
724
  class DeadSet < JobSet
630
725
  def initialize
631
- super 'dead'
726
+ super "dead"
632
727
  end
633
728
 
634
- def retry_all
635
- while size > 0
636
- each(&:retry)
729
+ def kill(message, opts = {})
730
+ now = Time.now.to_f
731
+ Sidekiq.redis do |conn|
732
+ conn.multi do
733
+ conn.zadd(name, now.to_s, message)
734
+ conn.zremrangebyscore(name, "-inf", now - self.class.timeout)
735
+ conn.zremrangebyrank(name, 0, - self.class.max_jobs)
736
+ end
737
+ end
738
+
739
+ if opts[:notify_failure] != false
740
+ job = Sidekiq.load_json(message)
741
+ r = RuntimeError.new("Job killed by API")
742
+ r.set_backtrace(caller)
743
+ Sidekiq.death_handlers.each do |handle|
744
+ handle.call(job, r)
745
+ end
637
746
  end
747
+ true
748
+ end
749
+
750
+ def retry_all
751
+ each(&:retry) while size > 0
638
752
  end
639
753
 
640
754
  def self.max_jobs
@@ -648,7 +762,7 @@ module Sidekiq
648
762
 
649
763
  ##
650
764
  # Enumerates the set of Sidekiq processes which are actively working
651
- # right now. Each process send a heartbeat to Redis every 5 seconds
765
+ # right now. Each process sends a heartbeat to Redis every 5 seconds
652
766
  # so this set should be relatively accurate, barring network partitions.
653
767
  #
654
768
  # Yields a Sidekiq::Process.
@@ -656,54 +770,60 @@ module Sidekiq
656
770
  class ProcessSet
657
771
  include Enumerable
658
772
 
659
- def initialize(clean_plz=true)
660
- self.class.cleanup if clean_plz
773
+ def initialize(clean_plz = true)
774
+ cleanup if clean_plz
661
775
  end
662
776
 
663
777
  # Cleans up dead processes recorded in Redis.
664
778
  # Returns the number of processes cleaned.
665
- def self.cleanup
779
+ def cleanup
666
780
  count = 0
667
781
  Sidekiq.redis do |conn|
668
- procs = conn.smembers('processes').sort
669
- heartbeats = conn.pipelined do
782
+ procs = conn.sscan_each("processes").to_a.sort
783
+ heartbeats = conn.pipelined {
670
784
  procs.each do |key|
671
- conn.hget(key, 'info')
785
+ conn.hget(key, "info")
672
786
  end
673
- end
787
+ }
674
788
 
675
789
  # the hash named key has an expiry of 60 seconds.
676
790
  # if it's not found, that means the process has not reported
677
791
  # in to Redis and probably died.
678
- to_prune = []
679
- heartbeats.each_with_index do |beat, i|
680
- to_prune << procs[i] if beat.nil?
681
- end
682
- count = conn.srem('processes', to_prune) unless to_prune.empty?
792
+ to_prune = procs.select.with_index { |proc, i|
793
+ heartbeats[i].nil?
794
+ }
795
+ count = conn.srem("processes", to_prune) unless to_prune.empty?
683
796
  end
684
797
  count
685
798
  end
686
799
 
687
800
  def each
688
- procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
801
+ result = Sidekiq.redis { |conn|
802
+ procs = conn.sscan_each("processes").to_a.sort
689
803
 
690
- Sidekiq.redis do |conn|
691
804
  # We're making a tradeoff here between consuming more memory instead of
692
805
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
693
806
  # you'll be happier this way
694
- result = conn.pipelined do
807
+ conn.pipelined do
695
808
  procs.each do |key|
696
- conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
809
+ conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
697
810
  end
698
811
  end
812
+ }
699
813
 
700
- result.each do |info, busy, at_s, quiet|
701
- hash = Sidekiq.load_json(info)
702
- yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
703
- end
704
- end
814
+ result.each do |info, busy, at_s, quiet, rss, rtt|
815
+ # If a process is stopped between when we query Redis for `procs` and
816
+ # when we query for `result`, we will have an item in `result` that is
817
+ # composed of `nil` values.
818
+ next if info.nil?
705
819
 
706
- nil
820
+ hash = Sidekiq.load_json(info)
821
+ yield Process.new(hash.merge("busy" => busy.to_i,
822
+ "beat" => at_s.to_f,
823
+ "quiet" => quiet,
824
+ "rss" => rss.to_i,
825
+ "rtt_us" => rtt.to_i))
826
+ end
707
827
  end
708
828
 
709
829
  # This method is not guaranteed accurate since it does not prune the set
@@ -711,7 +831,31 @@ module Sidekiq
711
831
  # contains Sidekiq processes which have sent a heartbeat within the last
712
832
  # 60 seconds.
713
833
  def size
714
- Sidekiq.redis { |conn| conn.scard('processes') }
834
+ Sidekiq.redis { |conn| conn.scard("processes") }
835
+ end
836
+
837
+ # Total number of threads available to execute jobs.
838
+ # For Sidekiq Enterprise customers this number (in production) must be
839
+ # less than or equal to your licensed concurrency.
840
+ def total_concurrency
841
+ sum { |x| x["concurrency"].to_i }
842
+ end
843
+
844
+ def total_rss_in_kb
845
+ sum { |x| x["rss"].to_i }
846
+ end
847
+ alias_method :total_rss, :total_rss_in_kb
848
+
849
+ # Returns the identity of the current cluster leader or "" if no leader.
850
+ # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
851
+ # or Sidekiq Pro.
852
+ def leader
853
+ @leader ||= begin
854
+ x = Sidekiq.redis { |c| c.get("dear-leader") }
855
+ # need a non-falsy value so we can memoize
856
+ x ||= ""
857
+ x
858
+ end
715
859
  end
716
860
  end
717
861
 
@@ -736,31 +880,39 @@ module Sidekiq
736
880
  end
737
881
 
738
882
  def tag
739
- self['tag']
883
+ self["tag"]
740
884
  end
741
885
 
742
886
  def labels
743
- Array(self['labels'])
887
+ Array(self["labels"])
744
888
  end
745
889
 
746
890
  def [](key)
747
891
  @attribs[key]
748
892
  end
749
893
 
894
+ def identity
895
+ self["identity"]
896
+ end
897
+
898
+ def queues
899
+ self["queues"]
900
+ end
901
+
750
902
  def quiet!
751
- signal('USR1')
903
+ signal("TSTP")
752
904
  end
753
905
 
754
906
  def stop!
755
- signal('TERM')
907
+ signal("TERM")
756
908
  end
757
909
 
758
910
  def dump_threads
759
- signal('TTIN')
911
+ signal("TTIN")
760
912
  end
761
913
 
762
914
  def stopping?
763
- self['quiet'] == 'true'
915
+ self["quiet"] == "true"
764
916
  end
765
917
 
766
918
  private
@@ -774,15 +926,11 @@ module Sidekiq
774
926
  end
775
927
  end
776
928
  end
777
-
778
- def identity
779
- self['identity']
780
- end
781
929
  end
782
930
 
783
931
  ##
784
- # A worker is a thread that is currently processing a job.
785
- # Programmatic access to the current active worker set.
932
+ # The WorkSet stores the work being done by this Sidekiq cluster.
933
+ # It tracks the process and thread working on each job.
786
934
  #
787
935
  # WARNING WARNING WARNING
788
936
  #
@@ -790,33 +938,40 @@ module Sidekiq
790
938
  # If you call #size => 5 and then expect #each to be
791
939
  # called 5 times, you're going to have a bad time.
792
940
  #
793
- # workers = Sidekiq::Workers.new
794
- # workers.size => 2
795
- # workers.each do |process_id, thread_id, work|
941
+ # works = Sidekiq::WorkSet.new
942
+ # works.size => 2
943
+ # works.each do |process_id, thread_id, work|
796
944
  # # process_id is a unique identifier per Sidekiq process
797
945
  # # thread_id is a unique identifier per thread
798
946
  # # work is a Hash which looks like:
799
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
947
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
800
948
  # # run_at is an epoch Integer.
801
949
  # end
802
950
  #
803
- class Workers
951
+ class WorkSet
804
952
  include Enumerable
805
953
 
806
- def each
954
+ def each(&block)
955
+ results = []
807
956
  Sidekiq.redis do |conn|
808
- procs = conn.smembers('processes')
957
+ procs = conn.sscan_each("processes").to_a
809
958
  procs.sort.each do |key|
810
- valid, workers = conn.pipelined do
811
- conn.exists(key)
959
+ valid, workers = conn.pipelined {
960
+ conn.exists?(key)
812
961
  conn.hgetall("#{key}:workers")
813
- end
962
+ }
814
963
  next unless valid
815
964
  workers.each_pair do |tid, json|
816
- yield key, tid, Sidekiq.load_json(json)
965
+ hsh = Sidekiq.load_json(json)
966
+ p = hsh["payload"]
967
+ # avoid breaking API, this is a side effect of the JSON optimization in #4316
968
+ hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
969
+ results << [key, tid, hsh]
817
970
  end
818
971
  end
819
972
  end
973
+
974
+ results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
820
975
  end
821
976
 
822
977
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -827,18 +982,21 @@ module Sidekiq
827
982
  # which can easily get out of sync with crashy processes.
828
983
  def size
829
984
  Sidekiq.redis do |conn|
830
- procs = conn.smembers('processes')
985
+ procs = conn.sscan_each("processes").to_a
831
986
  if procs.empty?
832
987
  0
833
988
  else
834
- conn.pipelined do
989
+ conn.pipelined {
835
990
  procs.each do |key|
836
- conn.hget(key, 'busy')
991
+ conn.hget(key, "busy")
837
992
  end
838
- end.map(&:to_i).inject(:+)
993
+ }.sum(&:to_i)
839
994
  end
840
995
  end
841
996
  end
842
997
  end
843
-
998
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
999
+ # Is "worker" a process, a type of job, a thread? Undefined!
1000
+ # WorkSet better describes the data.
1001
+ Workers = WorkSet
844
1002
  end