sidekiq 5.2.6 → 7.1.0

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +537 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +47 -50
  5. data/bin/sidekiq +22 -3
  6. data/bin/sidekiqload +213 -115
  7. data/bin/sidekiqmon +11 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +556 -351
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +203 -226
  15. data/lib/sidekiq/client.rb +121 -101
  16. data/lib/sidekiq/component.rb +68 -0
  17. data/lib/sidekiq/config.rb +274 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +49 -42
  21. data/lib/sidekiq/job.rb +374 -0
  22. data/lib/sidekiq/job_logger.rb +33 -7
  23. data/lib/sidekiq/job_retry.rb +131 -108
  24. data/lib/sidekiq/job_util.rb +105 -0
  25. data/lib/sidekiq/launcher.rb +203 -105
  26. data/lib/sidekiq/logger.rb +131 -0
  27. data/lib/sidekiq/manager.rb +43 -46
  28. data/lib/sidekiq/metrics/query.rb +153 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +136 -0
  31. data/lib/sidekiq/middleware/chain.rb +113 -56
  32. data/lib/sidekiq/middleware/current_attributes.rb +56 -0
  33. data/lib/sidekiq/middleware/i18n.rb +7 -7
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +146 -0
  36. data/lib/sidekiq/paginator.rb +28 -16
  37. data/lib/sidekiq/processor.rb +108 -107
  38. data/lib/sidekiq/rails.rb +49 -38
  39. data/lib/sidekiq/redis_client_adapter.rb +96 -0
  40. data/lib/sidekiq/redis_connection.rb +38 -107
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +111 -49
  43. data/lib/sidekiq/sd_notify.rb +149 -0
  44. data/lib/sidekiq/systemd.rb +24 -0
  45. data/lib/sidekiq/testing/inline.rb +6 -5
  46. data/lib/sidekiq/testing.rb +66 -84
  47. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  48. data/lib/sidekiq/version.rb +3 -1
  49. data/lib/sidekiq/web/action.rb +15 -11
  50. data/lib/sidekiq/web/application.rb +123 -79
  51. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  52. data/lib/sidekiq/web/helpers.rb +137 -106
  53. data/lib/sidekiq/web/router.rb +23 -19
  54. data/lib/sidekiq/web.rb +56 -107
  55. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  56. data/lib/sidekiq.rb +92 -182
  57. data/sidekiq.gemspec +25 -16
  58. data/web/assets/images/apple-touch-icon.png +0 -0
  59. data/web/assets/javascripts/application.js +130 -61
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/chart.min.js +13 -0
  62. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  63. data/web/assets/javascripts/dashboard-charts.js +166 -0
  64. data/web/assets/javascripts/dashboard.js +36 -292
  65. data/web/assets/javascripts/metrics.js +264 -0
  66. data/web/assets/stylesheets/application-dark.css +147 -0
  67. data/web/assets/stylesheets/application-rtl.css +2 -95
  68. data/web/assets/stylesheets/application.css +102 -522
  69. data/web/locales/ar.yml +71 -65
  70. data/web/locales/cs.yml +62 -62
  71. data/web/locales/da.yml +60 -53
  72. data/web/locales/de.yml +65 -53
  73. data/web/locales/el.yml +43 -24
  74. data/web/locales/en.yml +84 -66
  75. data/web/locales/es.yml +70 -54
  76. data/web/locales/fa.yml +65 -65
  77. data/web/locales/fr.yml +83 -62
  78. data/web/locales/gd.yml +99 -0
  79. data/web/locales/he.yml +65 -64
  80. data/web/locales/hi.yml +59 -59
  81. data/web/locales/it.yml +53 -53
  82. data/web/locales/ja.yml +75 -64
  83. data/web/locales/ko.yml +52 -52
  84. data/web/locales/lt.yml +83 -0
  85. data/web/locales/nb.yml +61 -61
  86. data/web/locales/nl.yml +52 -52
  87. data/web/locales/pl.yml +45 -45
  88. data/web/locales/pt-br.yml +63 -55
  89. data/web/locales/pt.yml +51 -51
  90. data/web/locales/ru.yml +68 -63
  91. data/web/locales/sv.yml +53 -53
  92. data/web/locales/ta.yml +60 -60
  93. data/web/locales/uk.yml +62 -61
  94. data/web/locales/ur.yml +64 -64
  95. data/web/locales/vi.yml +83 -0
  96. data/web/locales/zh-cn.yml +43 -16
  97. data/web/locales/zh-tw.yml +42 -8
  98. data/web/views/_footer.erb +6 -3
  99. data/web/views/_job_info.erb +21 -4
  100. data/web/views/_metrics_period_select.erb +12 -0
  101. data/web/views/_nav.erb +1 -1
  102. data/web/views/_paging.erb +2 -0
  103. data/web/views/_poll_link.erb +3 -6
  104. data/web/views/_summary.erb +7 -7
  105. data/web/views/busy.erb +75 -25
  106. data/web/views/dashboard.erb +58 -18
  107. data/web/views/dead.erb +3 -3
  108. data/web/views/layout.erb +3 -1
  109. data/web/views/metrics.erb +82 -0
  110. data/web/views/metrics_for_job.erb +68 -0
  111. data/web/views/morgue.erb +14 -15
  112. data/web/views/queue.erb +33 -24
  113. data/web/views/queues.erb +13 -3
  114. data/web/views/retries.erb +16 -17
  115. data/web/views/retry.erb +3 -3
  116. data/web/views/scheduled.erb +17 -15
  117. metadata +69 -69
  118. data/.github/contributing.md +0 -32
  119. data/.github/issue_template.md +0 -11
  120. data/.gitignore +0 -15
  121. data/.travis.yml +0 -11
  122. data/3.0-Upgrade.md +0 -70
  123. data/4.0-Upgrade.md +0 -53
  124. data/5.0-Upgrade.md +0 -56
  125. data/COMM-LICENSE +0 -97
  126. data/Ent-Changes.md +0 -238
  127. data/Gemfile +0 -23
  128. data/LICENSE +0 -9
  129. data/Pro-2.0-Upgrade.md +0 -138
  130. data/Pro-3.0-Upgrade.md +0 -44
  131. data/Pro-4.0-Upgrade.md +0 -35
  132. data/Pro-Changes.md +0 -759
  133. data/Rakefile +0 -9
  134. data/bin/sidekiqctl +0 -20
  135. data/code_of_conduct.md +0 -50
  136. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  137. data/lib/sidekiq/core_ext.rb +0 -1
  138. data/lib/sidekiq/ctl.rb +0 -221
  139. data/lib/sidekiq/delay.rb +0 -42
  140. data/lib/sidekiq/exception_handler.rb +0 -29
  141. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  142. data/lib/sidekiq/extensions/active_record.rb +0 -40
  143. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  144. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  145. data/lib/sidekiq/logging.rb +0 -122
  146. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  147. data/lib/sidekiq/util.rb +0 -66
  148. data/lib/sidekiq/worker.rb +0 -220
data/lib/sidekiq/api.rb CHANGED
@@ -1,26 +1,33 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq'
3
2
 
4
- module Sidekiq
3
+ require "sidekiq"
5
4
 
6
- module RedisScanner
7
- def sscan(conn, key)
8
- cursor = '0'
9
- result = []
10
- loop do
11
- cursor, values = conn.sscan(key, cursor)
12
- result.push(*values)
13
- break if cursor == '0'
14
- end
15
- result
16
- end
17
- end
5
+ require "zlib"
6
+ require "set"
7
+ require "base64"
18
8
 
19
- class Stats
20
- include RedisScanner
9
+ require "sidekiq/metrics/query"
21
10
 
11
+ #
12
+ # Sidekiq's Data API provides a Ruby object model on top
13
+ # of Sidekiq's runtime data in Redis. This API should never
14
+ # be used within application code for business logic.
15
+ #
16
+ # The Sidekiq server process never uses this API: all data
17
+ # manipulation is done directly for performance reasons to
18
+ # ensure we are using Redis as efficiently as possible at
19
+ # every callsite.
20
+ #
21
+
22
+ module Sidekiq
23
+ # Retrieve runtime statistics from Redis regarding
24
+ # this Sidekiq cluster.
25
+ #
26
+ # stat = Sidekiq::Stats.new
27
+ # stat.processed
28
+ class Stats
22
29
  def initialize
23
- fetch_stats!
30
+ fetch_stats_fast!
24
31
  end
25
32
 
26
33
  def processed
@@ -60,65 +67,96 @@ module Sidekiq
60
67
  end
61
68
 
62
69
  def queues
63
- Sidekiq::Stats::Queues.new.lengths
70
+ Sidekiq.redis do |conn|
71
+ queues = conn.sscan("queues").to_a
72
+
73
+ lengths = conn.pipelined { |pipeline|
74
+ queues.each do |queue|
75
+ pipeline.llen("queue:#{queue}")
76
+ end
77
+ }
78
+
79
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
80
+ array_of_arrays.to_h
81
+ end
64
82
  end
65
83
 
66
- def fetch_stats!
67
- pipe1_res = Sidekiq.redis do |conn|
68
- conn.pipelined do
69
- conn.get('stat:processed')
70
- conn.get('stat:failed')
71
- conn.zcard('schedule')
72
- conn.zcard('retry')
73
- conn.zcard('dead')
74
- conn.scard('processes')
75
- conn.lrange('queue:default', -1, -1)
84
+ # O(1) redis calls
85
+ # @api private
86
+ def fetch_stats_fast!
87
+ pipe1_res = Sidekiq.redis { |conn|
88
+ conn.pipelined do |pipeline|
89
+ pipeline.get("stat:processed")
90
+ pipeline.get("stat:failed")
91
+ pipeline.zcard("schedule")
92
+ pipeline.zcard("retry")
93
+ pipeline.zcard("dead")
94
+ pipeline.scard("processes")
95
+ pipeline.lrange("queue:default", -1, -1)
76
96
  end
77
- end
97
+ }
78
98
 
79
- processes = Sidekiq.redis do |conn|
80
- sscan(conn, 'processes')
99
+ default_queue_latency = if (entry = pipe1_res[6].first)
100
+ job = begin
101
+ Sidekiq.load_json(entry)
102
+ rescue
103
+ {}
104
+ end
105
+ now = Time.now.to_f
106
+ thence = job["enqueued_at"] || now
107
+ now - thence
108
+ else
109
+ 0
81
110
  end
82
111
 
83
- queues = Sidekiq.redis do |conn|
84
- sscan(conn, 'queues')
85
- end
112
+ @stats = {
113
+ processed: pipe1_res[0].to_i,
114
+ failed: pipe1_res[1].to_i,
115
+ scheduled_size: pipe1_res[2],
116
+ retry_size: pipe1_res[3],
117
+ dead_size: pipe1_res[4],
118
+ processes_size: pipe1_res[5],
119
+
120
+ default_queue_latency: default_queue_latency
121
+ }
122
+ end
123
+
124
+ # O(number of processes + number of queues) redis calls
125
+ # @api private
126
+ def fetch_stats_slow!
127
+ processes = Sidekiq.redis { |conn|
128
+ conn.sscan("processes").to_a
129
+ }
86
130
 
87
- pipe2_res = Sidekiq.redis do |conn|
88
- conn.pipelined do
89
- processes.each {|key| conn.hget(key, 'busy') }
90
- queues.each {|queue| conn.llen("queue:#{queue}") }
131
+ queues = Sidekiq.redis { |conn|
132
+ conn.sscan("queues").to_a
133
+ }
134
+
135
+ pipe2_res = Sidekiq.redis { |conn|
136
+ conn.pipelined do |pipeline|
137
+ processes.each { |key| pipeline.hget(key, "busy") }
138
+ queues.each { |queue| pipeline.llen("queue:#{queue}") }
91
139
  end
92
- end
140
+ }
93
141
 
94
142
  s = processes.size
95
- workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
96
- enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
143
+ workers_size = pipe2_res[0...s].sum(&:to_i)
144
+ enqueued = pipe2_res[s..].sum(&:to_i)
97
145
 
98
- default_queue_latency = if (entry = pipe1_res[6].first)
99
- job = Sidekiq.load_json(entry) rescue {}
100
- now = Time.now.to_f
101
- thence = job['enqueued_at'] || now
102
- now - thence
103
- else
104
- 0
105
- end
106
- @stats = {
107
- processed: pipe1_res[0].to_i,
108
- failed: pipe1_res[1].to_i,
109
- scheduled_size: pipe1_res[2],
110
- retry_size: pipe1_res[3],
111
- dead_size: pipe1_res[4],
112
- processes_size: pipe1_res[5],
113
-
114
- default_queue_latency: default_queue_latency,
115
- workers_size: workers_size,
116
- enqueued: enqueued
117
- }
146
+ @stats[:workers_size] = workers_size
147
+ @stats[:enqueued] = enqueued
148
+ @stats
149
+ end
150
+
151
+ # @api private
152
+ def fetch_stats!
153
+ fetch_stats_fast!
154
+ fetch_stats_slow!
118
155
  end
119
156
 
157
+ # @api private
120
158
  def reset(*stats)
121
- all = %w(failed processed)
159
+ all = %w[failed processed]
122
160
  stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
123
161
 
124
162
  mset_args = []
@@ -134,36 +172,14 @@ module Sidekiq
134
172
  private
135
173
 
136
174
  def stat(s)
137
- @stats[s]
138
- end
139
-
140
- class Queues
141
- include RedisScanner
142
-
143
- def lengths
144
- Sidekiq.redis do |conn|
145
- queues = sscan(conn, 'queues')
146
-
147
- lengths = conn.pipelined do
148
- queues.each do |queue|
149
- conn.llen("queue:#{queue}")
150
- end
151
- end
152
-
153
- i = 0
154
- array_of_arrays = queues.inject({}) do |memo, queue|
155
- memo[queue] = lengths[i]
156
- i += 1
157
- memo
158
- end.sort_by { |_, size| size }
159
-
160
- Hash[array_of_arrays.reverse]
161
- end
162
- end
175
+ fetch_stats_slow! if @stats[s].nil?
176
+ @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
163
177
  end
164
178
 
165
179
  class History
166
- def initialize(days_previous, start_date = nil)
180
+ def initialize(days_previous, start_date = nil, pool: nil)
181
+ # we only store five years of data in Redis
182
+ raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
167
183
  @days_previous = days_previous
168
184
  @start_date = start_date || Time.now.utc.to_date
169
185
  end
@@ -179,28 +195,17 @@ module Sidekiq
179
195
  private
180
196
 
181
197
  def date_stat_hash(stat)
182
- i = 0
183
198
  stat_hash = {}
184
- keys = []
185
- dates = []
186
-
187
- while i < @days_previous
188
- date = @start_date - i
189
- datestr = date.strftime("%Y-%m-%d")
190
- keys << "stat:#{stat}:#{datestr}"
191
- dates << datestr
192
- i += 1
193
- end
199
+ dates = @start_date.downto(@start_date - @days_previous + 1).map { |date|
200
+ date.strftime("%Y-%m-%d")
201
+ }
194
202
 
195
- begin
196
- Sidekiq.redis do |conn|
197
- conn.mget(keys).each_with_index do |value, idx|
198
- stat_hash[dates[idx]] = value ? value.to_i : 0
199
- end
203
+ keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
204
+
205
+ Sidekiq.redis do |conn|
206
+ conn.mget(keys).each_with_index do |value, idx|
207
+ stat_hash[dates[idx]] = value ? value.to_i : 0
200
208
  end
201
- rescue Redis::CommandError
202
- # mget will trigger a CROSSSLOT error when run against a Cluster
203
- # TODO Someone want to add Cluster support?
204
209
  end
205
210
 
206
211
  stat_hash
@@ -209,9 +214,10 @@ module Sidekiq
209
214
  end
210
215
 
211
216
  ##
212
- # Encapsulates a queue within Sidekiq.
217
+ # Represents a queue within Sidekiq.
213
218
  # Allows enumeration of all jobs within the queue
214
- # and deletion of jobs.
219
+ # and deletion of jobs. NB: this queue data is real-time
220
+ # and is changing within Redis moment by moment.
215
221
  #
216
222
  # queue = Sidekiq::Queue.new("mailer")
217
223
  # queue.each do |job|
@@ -219,30 +225,34 @@ module Sidekiq
219
225
  # job.args # => [1, 2, 3]
220
226
  # job.delete if job.jid == 'abcdef1234567890'
221
227
  # end
222
- #
223
228
  class Queue
224
229
  include Enumerable
225
- extend RedisScanner
226
230
 
227
231
  ##
228
- # Return all known queues within Redis.
232
+ # Fetch all known queues within Redis.
229
233
  #
234
+ # @return [Array<Sidekiq::Queue>]
230
235
  def self.all
231
- Sidekiq.redis { |c| sscan(c, 'queues') }.sort.map { |q| Sidekiq::Queue.new(q) }
236
+ Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
232
237
  end
233
238
 
234
239
  attr_reader :name
235
240
 
236
- def initialize(name="default")
241
+ # @param name [String] the name of the queue
242
+ def initialize(name = "default")
237
243
  @name = name.to_s
238
244
  @rname = "queue:#{name}"
239
245
  end
240
246
 
247
+ # The current size of the queue within Redis.
248
+ # This value is real-time and can change between calls.
249
+ #
250
+ # @return [Integer] the size
241
251
  def size
242
252
  Sidekiq.redis { |con| con.llen(@rname) }
243
253
  end
244
254
 
245
- # Sidekiq Pro overrides this
255
+ # @return [Boolean] if the queue is currently paused
246
256
  def paused?
247
257
  false
248
258
  end
@@ -251,15 +261,15 @@ module Sidekiq
251
261
  # Calculates this queue's latency, the difference in seconds since the oldest
252
262
  # job in the queue was enqueued.
253
263
  #
254
- # @return Float
264
+ # @return [Float] in seconds
255
265
  def latency
256
- entry = Sidekiq.redis do |conn|
266
+ entry = Sidekiq.redis { |conn|
257
267
  conn.lrange(@rname, -1, -1)
258
- end.first
268
+ }.first
259
269
  return 0 unless entry
260
270
  job = Sidekiq.load_json(entry)
261
271
  now = Time.now.to_f
262
- thence = job['enqueued_at'] || now
272
+ thence = job["enqueued_at"] || now
263
273
  now - thence
264
274
  end
265
275
 
@@ -269,16 +279,16 @@ module Sidekiq
269
279
  page = 0
270
280
  page_size = 50
271
281
 
272
- while true do
282
+ loop do
273
283
  range_start = page * page_size - deleted_size
274
- range_end = range_start + page_size - 1
275
- entries = Sidekiq.redis do |conn|
284
+ range_end = range_start + page_size - 1
285
+ entries = Sidekiq.redis { |conn|
276
286
  conn.lrange @rname, range_start, range_end
277
- end
287
+ }
278
288
  break if entries.empty?
279
289
  page += 1
280
290
  entries.each do |entry|
281
- yield Job.new(entry, @name)
291
+ yield JobRecord.new(entry, @name)
282
292
  end
283
293
  deleted_size = initial_size - size
284
294
  end
@@ -287,41 +297,63 @@ module Sidekiq
287
297
  ##
288
298
  # Find the job with the given JID within this queue.
289
299
  #
290
- # This is a slow, inefficient operation. Do not use under
291
- # normal conditions. Sidekiq Pro contains a faster version.
300
+ # This is a *slow, inefficient* operation. Do not use under
301
+ # normal conditions.
302
+ #
303
+ # @param jid [String] the job_id to look for
304
+ # @return [Sidekiq::JobRecord]
305
+ # @return [nil] if not found
292
306
  def find_job(jid)
293
307
  detect { |j| j.jid == jid }
294
308
  end
295
309
 
310
+ # delete all jobs within this queue
311
+ # @return [Boolean] true
296
312
  def clear
297
313
  Sidekiq.redis do |conn|
298
- conn.multi do
299
- conn.del(@rname)
300
- conn.srem("queues", name)
314
+ conn.multi do |transaction|
315
+ transaction.unlink(@rname)
316
+ transaction.srem("queues", [name])
301
317
  end
302
318
  end
319
+ true
303
320
  end
304
321
  alias_method :💣, :clear
322
+
323
+ # :nodoc:
324
+ # @api private
325
+ def as_json(options = nil)
326
+ {name: name} # 5336
327
+ end
305
328
  end
306
329
 
307
330
  ##
308
- # Encapsulates a pending job within a Sidekiq queue or
309
- # sorted set.
331
+ # Represents a pending job within a Sidekiq queue.
310
332
  #
311
333
  # The job should be considered immutable but may be
312
- # removed from the queue via Job#delete.
313
- #
314
- class Job
334
+ # removed from the queue via JobRecord#delete.
335
+ class JobRecord
336
+ # the parsed Hash of job data
337
+ # @!attribute [r] Item
315
338
  attr_reader :item
339
+ # the underlying String in Redis
340
+ # @!attribute [r] Value
316
341
  attr_reader :value
342
+ # the queue associated with this job
343
+ # @!attribute [r] Queue
344
+ attr_reader :queue
317
345
 
318
- def initialize(item, queue_name=nil)
346
+ # :nodoc:
347
+ # @api private
348
+ def initialize(item, queue_name = nil)
319
349
  @args = nil
320
350
  @value = item
321
351
  @item = item.is_a?(Hash) ? item : parse(item)
322
- @queue = queue_name || @item['queue']
352
+ @queue = queue_name || @item["queue"]
323
353
  end
324
354
 
355
+ # :nodoc:
356
+ # @api private
325
357
  def parse(item)
326
358
  Sidekiq.load_json(item)
327
359
  rescue JSON::ParserError
@@ -333,88 +365,99 @@ module Sidekiq
333
365
  {}
334
366
  end
335
367
 
368
+ # This is the job class which Sidekiq will execute. If using ActiveJob,
369
+ # this class will be the ActiveJob adapter class rather than a specific job.
336
370
  def klass
337
- self['class']
371
+ self["class"]
338
372
  end
339
373
 
340
374
  def display_class
341
375
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
342
- @klass ||= case klass
343
- when /\ASidekiq::Extensions::Delayed/
344
- safe_load(args[0], klass) do |target, method, _|
345
- "#{target}.#{method}"
346
- end
347
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
348
- job_class = @item['wrapped'] || args[0]
349
- if 'ActionMailer::DeliveryJob' == job_class
350
- # MailerClass#mailer_method
351
- args[0]['arguments'][0..1].join('#')
352
- else
353
- job_class
354
- end
355
- else
356
- klass
357
- end
376
+ @klass ||= self["display_class"] || begin
377
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
378
+ job_class = @item["wrapped"] || args[0]
379
+ if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
380
+ # MailerClass#mailer_method
381
+ args[0]["arguments"][0..1].join("#")
382
+ else
383
+ job_class
384
+ end
385
+ else
386
+ klass
387
+ end
388
+ end
358
389
  end
359
390
 
360
391
  def display_args
361
392
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
362
- @display_args ||= case klass
363
- when /\ASidekiq::Extensions::Delayed/
364
- safe_load(args[0], args) do |_, _, arg|
365
- arg
366
- end
367
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
368
- job_args = self['wrapped'] ? args[0]["arguments"] : []
369
- if 'ActionMailer::DeliveryJob' == (self['wrapped'] || args[0])
370
- # remove MailerClass, mailer_method and 'deliver_now'
371
- job_args.drop(3)
372
- else
373
- job_args
374
- end
375
- else
376
- if self['encrypt']
377
- # no point in showing 150+ bytes of random garbage
378
- args[-1] = '[encrypted data]'
379
- end
380
- args
381
- end
393
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
394
+ job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
395
+ if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
396
+ # remove MailerClass, mailer_method and 'deliver_now'
397
+ job_args.drop(3)
398
+ elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
399
+ # remove MailerClass, mailer_method and 'deliver_now'
400
+ job_args.drop(3).first.values_at("params", "args")
401
+ else
402
+ job_args
403
+ end
404
+ else
405
+ if self["encrypt"]
406
+ # no point in showing 150+ bytes of random garbage
407
+ args[-1] = "[encrypted data]"
408
+ end
409
+ args
410
+ end
382
411
  end
383
412
 
384
413
  def args
385
- @args || @item['args']
414
+ @args || @item["args"]
386
415
  end
387
416
 
388
417
  def jid
389
- self['jid']
418
+ self["jid"]
419
+ end
420
+
421
+ def bid
422
+ self["bid"]
390
423
  end
391
424
 
392
425
  def enqueued_at
393
- self['enqueued_at'] ? Time.at(self['enqueued_at']).utc : nil
426
+ self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
394
427
  end
395
428
 
396
429
  def created_at
397
- Time.at(self['created_at'] || self['enqueued_at'] || 0).utc
430
+ Time.at(self["created_at"] || self["enqueued_at"] || 0).utc
398
431
  end
399
432
 
400
- def queue
401
- @queue
433
+ def tags
434
+ self["tags"] || []
435
+ end
436
+
437
+ def error_backtrace
438
+ # Cache nil values
439
+ if defined?(@error_backtrace)
440
+ @error_backtrace
441
+ else
442
+ value = self["error_backtrace"]
443
+ @error_backtrace = value && uncompress_backtrace(value)
444
+ end
402
445
  end
403
446
 
404
447
  def latency
405
448
  now = Time.now.to_f
406
- now - (@item['enqueued_at'] || @item['created_at'] || now)
449
+ now - (@item["enqueued_at"] || @item["created_at"] || now)
407
450
  end
408
451
 
409
- ##
410
- # Remove this job from the queue.
452
+ # Remove this job from the queue
411
453
  def delete
412
- count = Sidekiq.redis do |conn|
454
+ count = Sidekiq.redis { |conn|
413
455
  conn.lrem("queue:#{@queue}", 1, @value)
414
- end
456
+ }
415
457
  count != 0
416
458
  end
417
459
 
460
+ # Access arbitrary attributes within the job hash
418
461
  def [](name)
419
462
  # nil will happen if the JSON fails to parse.
420
463
  # We don't guarantee Sidekiq will work with bad job JSON but we should
@@ -424,32 +467,58 @@ module Sidekiq
424
467
 
425
468
  private
426
469
 
427
- def safe_load(content, default)
428
- begin
429
- yield(*YAML.load(content))
430
- rescue => ex
431
- # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
432
- # memory yet so the YAML can't be loaded.
433
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == 'development'
434
- default
470
+ ACTIVE_JOB_PREFIX = "_aj_"
471
+ GLOBALID_KEY = "_aj_globalid"
472
+
473
+ def deserialize_argument(argument)
474
+ case argument
475
+ when Array
476
+ argument.map { |arg| deserialize_argument(arg) }
477
+ when Hash
478
+ if serialized_global_id?(argument)
479
+ argument[GLOBALID_KEY]
480
+ else
481
+ argument.transform_values { |v| deserialize_argument(v) }
482
+ .reject { |k, _| k.start_with?(ACTIVE_JOB_PREFIX) }
483
+ end
484
+ else
485
+ argument
435
486
  end
436
487
  end
488
+
489
+ def serialized_global_id?(hash)
490
+ hash.size == 1 && hash.include?(GLOBALID_KEY)
491
+ end
492
+
493
+ def uncompress_backtrace(backtrace)
494
+ decoded = Base64.decode64(backtrace)
495
+ uncompressed = Zlib::Inflate.inflate(decoded)
496
+ Sidekiq.load_json(uncompressed)
497
+ end
437
498
  end
438
499
 
439
- class SortedEntry < Job
500
+ # Represents a job within a Redis sorted set where the score
501
+ # represents a timestamp associated with the job. This timestamp
502
+ # could be the scheduled time for it to run (e.g. scheduled set),
503
+ # or the expiration date after which the entry should be deleted (e.g. dead set).
504
+ class SortedEntry < JobRecord
440
505
  attr_reader :score
441
506
  attr_reader :parent
442
507
 
508
+ # :nodoc:
509
+ # @api private
443
510
  def initialize(parent, score, item)
444
511
  super(item)
445
- @score = score
512
+ @score = Float(score)
446
513
  @parent = parent
447
514
  end
448
515
 
516
+ # The timestamp associated with this entry
449
517
  def at
450
518
  Time.at(score).utc
451
519
  end
452
520
 
521
+ # remove this entry from the sorted set
453
522
  def delete
454
523
  if @value
455
524
  @parent.delete_by_value(@parent.name, @value)
@@ -458,11 +527,17 @@ module Sidekiq
458
527
  end
459
528
  end
460
529
 
530
+ # Change the scheduled time for this job.
531
+ #
532
+ # @param at [Time] the new timestamp for this job
461
533
  def reschedule(at)
462
- delete
463
- @parent.schedule(at, item)
534
+ Sidekiq.redis do |conn|
535
+ conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
536
+ end
464
537
  end
465
538
 
539
+ # Enqueue this job from the scheduled or dead set so it will
540
+ # be executed at some point in the near future.
466
541
  def add_to_queue
467
542
  remove_job do |message|
468
543
  msg = Sidekiq.load_json(message)
@@ -470,16 +545,17 @@ module Sidekiq
470
545
  end
471
546
  end
472
547
 
548
+ # enqueue this job from the retry set so it will be executed
549
+ # at some point in the near future.
473
550
  def retry
474
551
  remove_job do |message|
475
552
  msg = Sidekiq.load_json(message)
476
- msg['retry_count'] -= 1 if msg['retry_count']
553
+ msg["retry_count"] -= 1 if msg["retry_count"]
477
554
  Sidekiq::Client.push(msg)
478
555
  end
479
556
  end
480
557
 
481
- ##
482
- # Place job in the dead set
558
+ # Move this job from its current set into the Dead set.
483
559
  def kill
484
560
  remove_job do |message|
485
561
  DeadSet.new.kill(message)
@@ -487,74 +563,109 @@ module Sidekiq
487
563
  end
488
564
 
489
565
  def error?
490
- !!item['error_class']
566
+ !!item["error_class"]
491
567
  end
492
568
 
493
569
  private
494
570
 
495
571
  def remove_job
496
572
  Sidekiq.redis do |conn|
497
- results = conn.multi do
498
- conn.zrangebyscore(parent.name, score, score)
499
- conn.zremrangebyscore(parent.name, score, score)
500
- end.first
573
+ results = conn.multi { |transaction|
574
+ transaction.zrange(parent.name, score, score, "BYSCORE")
575
+ transaction.zremrangebyscore(parent.name, score, score)
576
+ }.first
501
577
 
502
578
  if results.size == 1
503
579
  yield results.first
504
580
  else
505
581
  # multiple jobs with the same score
506
582
  # find the one with the right JID and push it
507
- hash = results.group_by do |message|
583
+ matched, nonmatched = results.partition { |message|
508
584
  if message.index(jid)
509
585
  msg = Sidekiq.load_json(message)
510
- msg['jid'] == jid
586
+ msg["jid"] == jid
511
587
  else
512
588
  false
513
589
  end
514
- end
590
+ }
515
591
 
516
- msg = hash.fetch(true, []).first
592
+ msg = matched.first
517
593
  yield msg if msg
518
594
 
519
595
  # push the rest back onto the sorted set
520
- conn.multi do
521
- hash.fetch(false, []).each do |message|
522
- conn.zadd(parent.name, score.to_f.to_s, message)
596
+ conn.multi do |transaction|
597
+ nonmatched.each do |message|
598
+ transaction.zadd(parent.name, score.to_f.to_s, message)
523
599
  end
524
600
  end
525
601
  end
526
602
  end
527
603
  end
528
-
529
604
  end
530
605
 
606
+ # Base class for all sorted sets within Sidekiq.
531
607
  class SortedSet
532
608
  include Enumerable
533
609
 
610
+ # Redis key of the set
611
+ # @!attribute [r] Name
534
612
  attr_reader :name
535
613
 
614
+ # :nodoc:
615
+ # @api private
536
616
  def initialize(name)
537
617
  @name = name
538
618
  @_size = size
539
619
  end
540
620
 
621
+ # real-time size of the set, will change
541
622
  def size
542
623
  Sidekiq.redis { |c| c.zcard(name) }
543
624
  end
544
625
 
626
+ # Scan through each element of the sorted set, yielding each to the supplied block.
627
+ # Please see Redis's <a href="https://redis.io/commands/scan/">SCAN documentation</a> for implementation details.
628
+ #
629
+ # @param match [String] a snippet or regexp to filter matches.
630
+ # @param count [Integer] number of elements to retrieve at a time, default 100
631
+ # @yieldparam [Sidekiq::SortedEntry] each entry
632
+ def scan(match, count = 100)
633
+ return to_enum(:scan, match, count) unless block_given?
634
+
635
+ match = "*#{match}*" unless match.include?("*")
636
+ Sidekiq.redis do |conn|
637
+ conn.zscan(name, match: match, count: count) do |entry, score|
638
+ yield SortedEntry.new(self, score, entry)
639
+ end
640
+ end
641
+ end
642
+
643
+ # @return [Boolean] always true
545
644
  def clear
546
645
  Sidekiq.redis do |conn|
547
- conn.del(name)
646
+ conn.unlink(name)
548
647
  end
648
+ true
549
649
  end
550
650
  alias_method :💣, :clear
651
+
652
+ # :nodoc:
653
+ # @api private
654
+ def as_json(options = nil)
655
+ {name: name} # 5336
656
+ end
551
657
  end
552
658
 
659
+ # Base class for all sorted sets which contain jobs, e.g. scheduled, retry and dead.
660
+ # Sidekiq Pro and Enterprise add additional sorted sets which do not contain job data,
661
+ # e.g. Batches.
553
662
  class JobSet < SortedSet
554
-
555
- def schedule(timestamp, message)
663
+ # Add a job with the associated timestamp to this set.
664
+ # @param timestamp [Time] the score for the job
665
+ # @param job [Hash] the job data
666
+ def schedule(timestamp, job)
556
667
  Sidekiq.redis do |conn|
557
- conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
668
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(job))
558
669
  end
559
670
  end
560
671
 
@@ -564,46 +675,66 @@ module Sidekiq
564
675
  page = -1
565
676
  page_size = 50
566
677
 
567
- while true do
678
+ loop do
568
679
  range_start = page * page_size + offset_size
569
- range_end = range_start + page_size - 1
570
- elements = Sidekiq.redis do |conn|
571
- conn.zrange name, range_start, range_end, with_scores: true
572
- end
680
+ range_end = range_start + page_size - 1
681
+ elements = Sidekiq.redis { |conn|
682
+ conn.zrange name, range_start, range_end, withscores: true
683
+ }
573
684
  break if elements.empty?
574
685
  page -= 1
575
- elements.reverse.each do |element, score|
686
+ elements.reverse_each do |element, score|
576
687
  yield SortedEntry.new(self, score, element)
577
688
  end
578
689
  offset_size = initial_size - @_size
579
690
  end
580
691
  end
581
692
 
693
+ ##
694
+ # Fetch jobs that match a given time or Range. Job ID is an
695
+ # optional second argument.
696
+ #
697
+ # @param score [Time,Range] a specific timestamp or range
698
+ # @param jid [String, optional] find a specific JID within the score
699
+ # @return [Array<SortedEntry>] any results found, can be empty
582
700
  def fetch(score, jid = nil)
583
- elements = Sidekiq.redis do |conn|
584
- conn.zrangebyscore(name, score, score)
585
- end
586
-
587
- elements.inject([]) do |result, element|
588
- entry = SortedEntry.new(self, score, element)
589
- if jid
590
- result << entry if entry.jid == jid
701
+ begin_score, end_score =
702
+ if score.is_a?(Range)
703
+ [score.first, score.last]
591
704
  else
592
- result << entry
705
+ [score, score]
593
706
  end
594
- result
707
+
708
+ elements = Sidekiq.redis { |conn|
709
+ conn.zrange(name, begin_score, end_score, "BYSCORE", withscores: true)
710
+ }
711
+
712
+ elements.each_with_object([]) do |element, result|
713
+ data, job_score = element
714
+ entry = SortedEntry.new(self, job_score, data)
715
+ result << entry if jid.nil? || entry.jid == jid
595
716
  end
596
717
  end
597
718
 
598
719
  ##
599
720
  # Find the job with the given JID within this sorted set.
721
+ # *This is a slow O(n) operation*. Do not use for app logic.
600
722
  #
601
- # This is a slow, inefficient operation. Do not use under
602
- # normal conditions. Sidekiq Pro contains a faster version.
723
+ # @param jid [String] the job identifier
724
+ # @return [SortedEntry] the record or nil
603
725
  def find_job(jid)
604
- self.detect { |j| j.jid == jid }
726
+ Sidekiq.redis do |conn|
727
+ conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
728
+ job = Sidekiq.load_json(entry)
729
+ matched = job["jid"] == jid
730
+ return SortedEntry.new(self, score, entry) if matched
731
+ end
732
+ end
733
+ nil
605
734
  end
606
735
 
736
+ # :nodoc:
737
+ # @api private
607
738
  def delete_by_value(name, value)
608
739
  Sidekiq.redis do |conn|
609
740
  ret = conn.zrem(name, value)
@@ -612,17 +743,20 @@ module Sidekiq
612
743
  end
613
744
  end
614
745
 
746
+ # :nodoc:
747
+ # @api private
615
748
  def delete_by_jid(score, jid)
616
749
  Sidekiq.redis do |conn|
617
- elements = conn.zrangebyscore(name, score, score)
750
+ elements = conn.zrange(name, score, score, "BYSCORE")
618
751
  elements.each do |element|
619
- message = Sidekiq.load_json(element)
620
- if message["jid"] == jid
621
- ret = conn.zrem(name, element)
622
- @_size -= 1 if ret
623
- break ret
752
+ if element.index(jid)
753
+ message = Sidekiq.load_json(element)
754
+ if message["jid"] == jid
755
+ ret = conn.zrem(name, element)
756
+ @_size -= 1 if ret
757
+ break ret
758
+ end
624
759
  end
625
- false
626
760
  end
627
761
  end
628
762
  end
@@ -631,68 +765,62 @@ module Sidekiq
631
765
  end
632
766
 
633
767
  ##
634
- # Allows enumeration of scheduled jobs within Sidekiq.
768
+ # The set of scheduled jobs within Sidekiq.
635
769
  # Based on this, you can search/filter for jobs. Here's an
636
- # example where I'm selecting all jobs of a certain type
637
- # and deleting them from the schedule queue.
770
+ # example where I'm selecting jobs based on some complex logic
771
+ # and deleting them from the scheduled set.
772
+ #
773
+ # See the API wiki page for usage notes and examples.
638
774
  #
639
- # r = Sidekiq::ScheduledSet.new
640
- # r.select do |scheduled|
641
- # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
642
- # scheduled.args[0] == 'User' &&
643
- # scheduled.args[1] == 'setup_new_subscriber'
644
- # end.map(&:delete)
645
775
  class ScheduledSet < JobSet
646
776
  def initialize
647
- super 'schedule'
777
+ super "schedule"
648
778
  end
649
779
  end
650
780
 
651
781
  ##
652
- # Allows enumeration of retries within Sidekiq.
782
+ # The set of retries within Sidekiq.
653
783
  # Based on this, you can search/filter for jobs. Here's an
654
784
  # example where I'm selecting all jobs of a certain type
655
785
  # and deleting them from the retry queue.
656
786
  #
657
- # r = Sidekiq::RetrySet.new
658
- # r.select do |retri|
659
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
660
- # retri.args[0] == 'User' &&
661
- # retri.args[1] == 'setup_new_subscriber'
662
- # end.map(&:delete)
787
+ # See the API wiki page for usage notes and examples.
788
+ #
663
789
  class RetrySet < JobSet
664
790
  def initialize
665
- super 'retry'
791
+ super "retry"
666
792
  end
667
793
 
794
+ # Enqueues all jobs pending within the retry set.
668
795
  def retry_all
669
- while size > 0
670
- each(&:retry)
671
- end
796
+ each(&:retry) while size > 0
672
797
  end
673
798
 
799
+ # Kills all jobs pending within the retry set.
674
800
  def kill_all
675
- while size > 0
676
- each(&:kill)
677
- end
801
+ each(&:kill) while size > 0
678
802
  end
679
803
  end
680
804
 
681
805
  ##
682
- # Allows enumeration of dead jobs within Sidekiq.
806
+ # The set of dead jobs within Sidekiq. Dead jobs have failed all of
807
+ # their retries and are helding in this set pending some sort of manual
808
+ # fix. They will be removed after 6 months (dead_timeout) if not.
683
809
  #
684
810
  class DeadSet < JobSet
685
811
  def initialize
686
- super 'dead'
812
+ super "dead"
687
813
  end
688
814
 
689
- def kill(message, opts={})
815
+ # Add the given job to the Dead set.
816
+ # @param message [String] the job data as JSON
817
+ def kill(message, opts = {})
690
818
  now = Time.now.to_f
691
819
  Sidekiq.redis do |conn|
692
- conn.multi do
693
- conn.zadd(name, now.to_s, message)
694
- conn.zremrangebyscore(name, '-inf', now - self.class.timeout)
695
- conn.zremrangebyrank(name, 0, - self.class.max_jobs)
820
+ conn.multi do |transaction|
821
+ transaction.zadd(name, now.to_s, message)
822
+ transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
823
+ transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
696
824
  end
697
825
  end
698
826
 
@@ -700,110 +828,143 @@ module Sidekiq
700
828
  job = Sidekiq.load_json(message)
701
829
  r = RuntimeError.new("Job killed by API")
702
830
  r.set_backtrace(caller)
703
- Sidekiq.death_handlers.each do |handle|
831
+ Sidekiq.default_configuration.death_handlers.each do |handle|
704
832
  handle.call(job, r)
705
833
  end
706
834
  end
707
835
  true
708
836
  end
709
837
 
838
+ # Enqueue all dead jobs
710
839
  def retry_all
711
- while size > 0
712
- each(&:retry)
713
- end
714
- end
715
-
716
- def self.max_jobs
717
- Sidekiq.options[:dead_max_jobs]
718
- end
719
-
720
- def self.timeout
721
- Sidekiq.options[:dead_timeout_in_seconds]
840
+ each(&:retry) while size > 0
722
841
  end
723
842
  end
724
843
 
725
844
  ##
726
845
  # Enumerates the set of Sidekiq processes which are actively working
727
- # right now. Each process send a heartbeat to Redis every 5 seconds
846
+ # right now. Each process sends a heartbeat to Redis every 5 seconds
728
847
  # so this set should be relatively accurate, barring network partitions.
729
848
  #
730
- # Yields a Sidekiq::Process.
849
+ # @yieldparam [Sidekiq::Process]
731
850
  #
732
851
  class ProcessSet
733
852
  include Enumerable
734
- include RedisScanner
735
853
 
736
- def initialize(clean_plz=true)
854
+ def self.[](identity)
855
+ exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
856
+ conn.multi { |transaction|
857
+ transaction.sismember("processes", identity)
858
+ transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
859
+ }
860
+ }
861
+
862
+ return nil if exists == 0 || info.nil?
863
+
864
+ hash = Sidekiq.load_json(info)
865
+ Process.new(hash.merge("busy" => busy.to_i,
866
+ "beat" => beat.to_f,
867
+ "quiet" => quiet,
868
+ "rss" => rss.to_i,
869
+ "rtt_us" => rtt_us.to_i))
870
+ end
871
+
872
+ # :nodoc:
873
+ # @api private
874
+ def initialize(clean_plz = true)
737
875
  cleanup if clean_plz
738
876
  end
739
877
 
740
878
  # Cleans up dead processes recorded in Redis.
741
879
  # Returns the number of processes cleaned.
880
+ # :nodoc:
881
+ # @api private
742
882
  def cleanup
883
+ # dont run cleanup more than once per minute
884
+ return 0 unless Sidekiq.redis { |conn| conn.set("process_cleanup", "1", nx: true, ex: 60) }
885
+
743
886
  count = 0
744
887
  Sidekiq.redis do |conn|
745
- procs = sscan(conn, 'processes').sort
746
- heartbeats = conn.pipelined do
888
+ procs = conn.sscan("processes").to_a
889
+ heartbeats = conn.pipelined { |pipeline|
747
890
  procs.each do |key|
748
- conn.hget(key, 'info')
891
+ pipeline.hget(key, "info")
749
892
  end
750
- end
893
+ }
751
894
 
752
895
  # the hash named key has an expiry of 60 seconds.
753
896
  # if it's not found, that means the process has not reported
754
897
  # in to Redis and probably died.
755
- to_prune = []
756
- heartbeats.each_with_index do |beat, i|
757
- to_prune << procs[i] if beat.nil?
758
- end
759
- count = conn.srem('processes', to_prune) unless to_prune.empty?
898
+ to_prune = procs.select.with_index { |proc, i|
899
+ heartbeats[i].nil?
900
+ }
901
+ count = conn.srem("processes", to_prune) unless to_prune.empty?
760
902
  end
761
903
  count
762
904
  end
763
905
 
764
906
  def each
765
- procs = Sidekiq.redis { |conn| sscan(conn, 'processes') }.sort
907
+ result = Sidekiq.redis { |conn|
908
+ procs = conn.sscan("processes").to_a.sort
766
909
 
767
- Sidekiq.redis do |conn|
768
910
  # We're making a tradeoff here between consuming more memory instead of
769
911
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
770
912
  # you'll be happier this way
771
- result = conn.pipelined do
913
+ conn.pipelined do |pipeline|
772
914
  procs.each do |key|
773
- conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
915
+ pipeline.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
774
916
  end
775
917
  end
918
+ }
776
919
 
777
- result.each do |info, busy, at_s, quiet|
778
- # If a process is stopped between when we query Redis for `procs` and
779
- # when we query for `result`, we will have an item in `result` that is
780
- # composed of `nil` values.
781
- next if info.nil?
782
-
783
- hash = Sidekiq.load_json(info)
784
- yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
785
- end
920
+ result.each do |info, busy, beat, quiet, rss, rtt_us|
921
+ # If a process is stopped between when we query Redis for `procs` and
922
+ # when we query for `result`, we will have an item in `result` that is
923
+ # composed of `nil` values.
924
+ next if info.nil?
925
+
926
+ hash = Sidekiq.load_json(info)
927
+ yield Process.new(hash.merge("busy" => busy.to_i,
928
+ "beat" => beat.to_f,
929
+ "quiet" => quiet,
930
+ "rss" => rss.to_i,
931
+ "rtt_us" => rtt_us.to_i))
786
932
  end
787
-
788
- nil
789
933
  end
790
934
 
791
935
  # This method is not guaranteed accurate since it does not prune the set
792
936
  # based on current heartbeat. #each does that and ensures the set only
793
937
  # contains Sidekiq processes which have sent a heartbeat within the last
794
938
  # 60 seconds.
939
+ # @return [Integer] current number of registered Sidekiq processes
795
940
  def size
796
- Sidekiq.redis { |conn| conn.scard('processes') }
941
+ Sidekiq.redis { |conn| conn.scard("processes") }
797
942
  end
798
943
 
944
+ # Total number of threads available to execute jobs.
945
+ # For Sidekiq Enterprise customers this number (in production) must be
946
+ # less than or equal to your licensed concurrency.
947
+ # @return [Integer] the sum of process concurrency
948
+ def total_concurrency
949
+ sum { |x| x["concurrency"].to_i }
950
+ end
951
+
952
+ # @return [Integer] total amount of RSS memory consumed by Sidekiq processes
953
+ def total_rss_in_kb
954
+ sum { |x| x["rss"].to_i }
955
+ end
956
+ alias_method :total_rss, :total_rss_in_kb
957
+
799
958
  # Returns the identity of the current cluster leader or "" if no leader.
800
959
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
801
960
  # or Sidekiq Pro.
961
+ # @return [String] Identity of cluster leader
962
+ # @return [String] empty string if no leader
802
963
  def leader
803
964
  @leader ||= begin
804
- x = Sidekiq.redis {|c| c.get("dear-leader") }
965
+ x = Sidekiq.redis { |c| c.get("dear-leader") }
805
966
  # need a non-falsy value so we can memoize
806
- x = "" unless x
967
+ x ||= ""
807
968
  x
808
969
  end
809
970
  end
@@ -823,18 +984,21 @@ module Sidekiq
823
984
  # 'busy' => 10,
824
985
  # 'beat' => <last heartbeat>,
825
986
  # 'identity' => <unique string identifying the process>,
987
+ # 'embedded' => true,
826
988
  # }
827
989
  class Process
990
+ # :nodoc:
991
+ # @api private
828
992
  def initialize(hash)
829
993
  @attribs = hash
830
994
  end
831
995
 
832
996
  def tag
833
- self['tag']
997
+ self["tag"]
834
998
  end
835
999
 
836
1000
  def labels
837
- Array(self['labels'])
1001
+ self["labels"].to_a
838
1002
  end
839
1003
 
840
1004
  def [](key)
@@ -842,23 +1006,56 @@ module Sidekiq
842
1006
  end
843
1007
 
844
1008
  def identity
845
- self['identity']
1009
+ self["identity"]
846
1010
  end
847
1011
 
1012
+ def queues
1013
+ self["queues"]
1014
+ end
1015
+
1016
+ def weights
1017
+ self["weights"]
1018
+ end
1019
+
1020
+ def version
1021
+ self["version"]
1022
+ end
1023
+
1024
+ def embedded?
1025
+ self["embedded"]
1026
+ end
1027
+
1028
+ # Signal this process to stop processing new jobs.
1029
+ # It will continue to execute jobs it has already fetched.
1030
+ # This method is *asynchronous* and it can take 5-10
1031
+ # seconds for the process to quiet.
848
1032
  def quiet!
849
- signal('TSTP')
1033
+ raise "Can't quiet an embedded process" if embedded?
1034
+
1035
+ signal("TSTP")
850
1036
  end
851
1037
 
1038
+ # Signal this process to shutdown.
1039
+ # It will shutdown within its configured :timeout value, default 25 seconds.
1040
+ # This method is *asynchronous* and it can take 5-10
1041
+ # seconds for the process to start shutting down.
852
1042
  def stop!
853
- signal('TERM')
1043
+ raise "Can't stop an embedded process" if embedded?
1044
+
1045
+ signal("TERM")
854
1046
  end
855
1047
 
1048
+ # Signal this process to log backtraces for all threads.
1049
+ # Useful if you have a frozen or deadlocked process which is
1050
+ # still sending a heartbeat.
1051
+ # This method is *asynchronous* and it can take 5-10 seconds.
856
1052
  def dump_threads
857
- signal('TTIN')
1053
+ signal("TTIN")
858
1054
  end
859
1055
 
1056
+ # @return [Boolean] true if this process is quiet or shutting down
860
1057
  def stopping?
861
- self['quiet'] == 'true'
1058
+ self["quiet"] == "true"
862
1059
  end
863
1060
 
864
1061
  private
@@ -866,18 +1063,17 @@ module Sidekiq
866
1063
  def signal(sig)
867
1064
  key = "#{identity}-signals"
868
1065
  Sidekiq.redis do |c|
869
- c.multi do
870
- c.lpush(key, sig)
871
- c.expire(key, 60)
1066
+ c.multi do |transaction|
1067
+ transaction.lpush(key, sig)
1068
+ transaction.expire(key, 60)
872
1069
  end
873
1070
  end
874
1071
  end
875
-
876
1072
  end
877
1073
 
878
1074
  ##
879
- # A worker is a thread that is currently processing a job.
880
- # Programmatic access to the current active worker set.
1075
+ # The WorkSet stores the work being done by this Sidekiq cluster.
1076
+ # It tracks the process and thread working on each job.
881
1077
  #
882
1078
  # WARNING WARNING WARNING
883
1079
  #
@@ -885,34 +1081,40 @@ module Sidekiq
885
1081
  # If you call #size => 5 and then expect #each to be
886
1082
  # called 5 times, you're going to have a bad time.
887
1083
  #
888
- # workers = Sidekiq::Workers.new
889
- # workers.size => 2
890
- # workers.each do |process_id, thread_id, work|
1084
+ # works = Sidekiq::WorkSet.new
1085
+ # works.size => 2
1086
+ # works.each do |process_id, thread_id, work|
891
1087
  # # process_id is a unique identifier per Sidekiq process
892
1088
  # # thread_id is a unique identifier per thread
893
1089
  # # work is a Hash which looks like:
894
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
1090
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
895
1091
  # # run_at is an epoch Integer.
896
1092
  # end
897
1093
  #
898
- class Workers
1094
+ class WorkSet
899
1095
  include Enumerable
900
- include RedisScanner
901
1096
 
902
- def each
1097
+ def each(&block)
1098
+ results = []
1099
+ procs = nil
1100
+ all_works = nil
1101
+
903
1102
  Sidekiq.redis do |conn|
904
- procs = sscan(conn, 'processes')
905
- procs.sort.each do |key|
906
- valid, workers = conn.pipelined do
907
- conn.exists(key)
908
- conn.hgetall("#{key}:workers")
909
- end
910
- next unless valid
911
- workers.each_pair do |tid, json|
912
- yield key, tid, Sidekiq.load_json(json)
1103
+ procs = conn.sscan("processes").to_a.sort
1104
+ all_works = conn.pipelined do |pipeline|
1105
+ procs.each do |key|
1106
+ pipeline.hgetall("#{key}:work")
913
1107
  end
914
1108
  end
915
1109
  end
1110
+
1111
+ procs.zip(all_works).each do |key, workers|
1112
+ workers.each_pair do |tid, json|
1113
+ results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
1114
+ end
1115
+ end
1116
+
1117
+ results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
916
1118
  end
917
1119
 
918
1120
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -923,18 +1125,21 @@ module Sidekiq
923
1125
  # which can easily get out of sync with crashy processes.
924
1126
  def size
925
1127
  Sidekiq.redis do |conn|
926
- procs = sscan(conn, 'processes')
1128
+ procs = conn.sscan("processes").to_a
927
1129
  if procs.empty?
928
1130
  0
929
1131
  else
930
- conn.pipelined do
1132
+ conn.pipelined { |pipeline|
931
1133
  procs.each do |key|
932
- conn.hget(key, 'busy')
1134
+ pipeline.hget(key, "busy")
933
1135
  end
934
- end.map(&:to_i).inject(:+)
1136
+ }.sum(&:to_i)
935
1137
  end
936
1138
  end
937
1139
  end
938
1140
  end
939
-
1141
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
1142
+ # Is "worker" a process, a type of job, a thread? Undefined!
1143
+ # WorkSet better describes the data.
1144
+ Workers = WorkSet
940
1145
  end