sr-sidekiq 4.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (186) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/3.0-Upgrade.md +70 -0
  4. data/4.0-Upgrade.md +50 -0
  5. data/COMM-LICENSE (sidekiq) +95 -0
  6. data/Changes.md +1241 -0
  7. data/Ent-Changes.md +112 -0
  8. data/Gemfile +29 -0
  9. data/LICENSE (sidekiq) +9 -0
  10. data/LICENSE (sr-sidekiq) +5 -0
  11. data/Pro-2.0-Upgrade.md +138 -0
  12. data/Pro-3.0-Upgrade.md +44 -0
  13. data/Pro-Changes.md +539 -0
  14. data/README.md +8 -0
  15. data/Rakefile +9 -0
  16. data/bin/sidekiq +18 -0
  17. data/bin/sidekiqctl +99 -0
  18. data/bin/sidekiqload +167 -0
  19. data/code_of_conduct.md +50 -0
  20. data/lib/generators/sidekiq/templates/worker.rb.erb +9 -0
  21. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +6 -0
  22. data/lib/generators/sidekiq/templates/worker_test.rb.erb +8 -0
  23. data/lib/generators/sidekiq/worker_generator.rb +49 -0
  24. data/lib/sidekiq.rb +237 -0
  25. data/lib/sidekiq/api.rb +844 -0
  26. data/lib/sidekiq/cli.rb +389 -0
  27. data/lib/sidekiq/client.rb +260 -0
  28. data/lib/sidekiq/core_ext.rb +106 -0
  29. data/lib/sidekiq/exception_handler.rb +31 -0
  30. data/lib/sidekiq/extensions/action_mailer.rb +57 -0
  31. data/lib/sidekiq/extensions/active_record.rb +40 -0
  32. data/lib/sidekiq/extensions/class_methods.rb +40 -0
  33. data/lib/sidekiq/extensions/generic_proxy.rb +25 -0
  34. data/lib/sidekiq/fetch.rb +81 -0
  35. data/lib/sidekiq/launcher.rb +160 -0
  36. data/lib/sidekiq/logging.rb +106 -0
  37. data/lib/sidekiq/manager.rb +137 -0
  38. data/lib/sidekiq/middleware/chain.rb +150 -0
  39. data/lib/sidekiq/middleware/i18n.rb +42 -0
  40. data/lib/sidekiq/middleware/server/active_record.rb +13 -0
  41. data/lib/sidekiq/middleware/server/logging.rb +40 -0
  42. data/lib/sidekiq/middleware/server/retry_jobs.rb +205 -0
  43. data/lib/sidekiq/paginator.rb +43 -0
  44. data/lib/sidekiq/processor.rb +186 -0
  45. data/lib/sidekiq/rails.rb +39 -0
  46. data/lib/sidekiq/redis_connection.rb +97 -0
  47. data/lib/sidekiq/scheduled.rb +146 -0
  48. data/lib/sidekiq/testing.rb +316 -0
  49. data/lib/sidekiq/testing/inline.rb +29 -0
  50. data/lib/sidekiq/util.rb +62 -0
  51. data/lib/sidekiq/version.rb +4 -0
  52. data/lib/sidekiq/web.rb +278 -0
  53. data/lib/sidekiq/web_helpers.rb +255 -0
  54. data/lib/sidekiq/worker.rb +121 -0
  55. data/sidekiq.gemspec +26 -0
  56. data/sr-sidekiq-4.1.3.gem +0 -0
  57. data/sr-sidekiq-4.1.4.gem +0 -0
  58. data/sr-sidekiq-4.1.5.gem +0 -0
  59. data/test/config.yml +9 -0
  60. data/test/env_based_config.yml +11 -0
  61. data/test/fake_env.rb +1 -0
  62. data/test/fixtures/en.yml +2 -0
  63. data/test/helper.rb +75 -0
  64. data/test/test_actors.rb +138 -0
  65. data/test/test_api.rb +528 -0
  66. data/test/test_cli.rb +406 -0
  67. data/test/test_client.rb +262 -0
  68. data/test/test_exception_handler.rb +56 -0
  69. data/test/test_extensions.rb +127 -0
  70. data/test/test_fetch.rb +50 -0
  71. data/test/test_launcher.rb +85 -0
  72. data/test/test_logging.rb +35 -0
  73. data/test/test_manager.rb +50 -0
  74. data/test/test_middleware.rb +158 -0
  75. data/test/test_processor.rb +201 -0
  76. data/test/test_rails.rb +22 -0
  77. data/test/test_redis_connection.rb +127 -0
  78. data/test/test_retry.rb +326 -0
  79. data/test/test_retry_exhausted.rb +149 -0
  80. data/test/test_scheduled.rb +115 -0
  81. data/test/test_scheduling.rb +50 -0
  82. data/test/test_sidekiq.rb +107 -0
  83. data/test/test_testing.rb +143 -0
  84. data/test/test_testing_fake.rb +357 -0
  85. data/test/test_testing_inline.rb +94 -0
  86. data/test/test_util.rb +13 -0
  87. data/test/test_web.rb +614 -0
  88. data/test/test_web_helpers.rb +54 -0
  89. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  90. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  91. data/web/assets/images/favicon.ico +0 -0
  92. data/web/assets/images/logo.png +0 -0
  93. data/web/assets/images/status-sd8051fd480.png +0 -0
  94. data/web/assets/images/status/active.png +0 -0
  95. data/web/assets/images/status/idle.png +0 -0
  96. data/web/assets/javascripts/application.js +88 -0
  97. data/web/assets/javascripts/dashboard.js +300 -0
  98. data/web/assets/javascripts/locales/README.md +27 -0
  99. data/web/assets/javascripts/locales/jquery.timeago.ar.js +96 -0
  100. data/web/assets/javascripts/locales/jquery.timeago.bg.js +18 -0
  101. data/web/assets/javascripts/locales/jquery.timeago.bs.js +49 -0
  102. data/web/assets/javascripts/locales/jquery.timeago.ca.js +18 -0
  103. data/web/assets/javascripts/locales/jquery.timeago.cs.js +18 -0
  104. data/web/assets/javascripts/locales/jquery.timeago.cy.js +20 -0
  105. data/web/assets/javascripts/locales/jquery.timeago.da.js +18 -0
  106. data/web/assets/javascripts/locales/jquery.timeago.de.js +18 -0
  107. data/web/assets/javascripts/locales/jquery.timeago.el.js +18 -0
  108. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +20 -0
  109. data/web/assets/javascripts/locales/jquery.timeago.en.js +20 -0
  110. data/web/assets/javascripts/locales/jquery.timeago.es.js +18 -0
  111. data/web/assets/javascripts/locales/jquery.timeago.et.js +18 -0
  112. data/web/assets/javascripts/locales/jquery.timeago.fa.js +22 -0
  113. data/web/assets/javascripts/locales/jquery.timeago.fi.js +28 -0
  114. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +16 -0
  115. data/web/assets/javascripts/locales/jquery.timeago.fr.js +17 -0
  116. data/web/assets/javascripts/locales/jquery.timeago.he.js +18 -0
  117. data/web/assets/javascripts/locales/jquery.timeago.hr.js +49 -0
  118. data/web/assets/javascripts/locales/jquery.timeago.hu.js +18 -0
  119. data/web/assets/javascripts/locales/jquery.timeago.hy.js +18 -0
  120. data/web/assets/javascripts/locales/jquery.timeago.id.js +18 -0
  121. data/web/assets/javascripts/locales/jquery.timeago.it.js +16 -0
  122. data/web/assets/javascripts/locales/jquery.timeago.ja.js +19 -0
  123. data/web/assets/javascripts/locales/jquery.timeago.ko.js +17 -0
  124. data/web/assets/javascripts/locales/jquery.timeago.lt.js +20 -0
  125. data/web/assets/javascripts/locales/jquery.timeago.mk.js +20 -0
  126. data/web/assets/javascripts/locales/jquery.timeago.nl.js +20 -0
  127. data/web/assets/javascripts/locales/jquery.timeago.no.js +18 -0
  128. data/web/assets/javascripts/locales/jquery.timeago.pl.js +31 -0
  129. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +16 -0
  130. data/web/assets/javascripts/locales/jquery.timeago.pt.js +16 -0
  131. data/web/assets/javascripts/locales/jquery.timeago.ro.js +18 -0
  132. data/web/assets/javascripts/locales/jquery.timeago.rs.js +49 -0
  133. data/web/assets/javascripts/locales/jquery.timeago.ru.js +34 -0
  134. data/web/assets/javascripts/locales/jquery.timeago.sk.js +18 -0
  135. data/web/assets/javascripts/locales/jquery.timeago.sl.js +44 -0
  136. data/web/assets/javascripts/locales/jquery.timeago.sv.js +18 -0
  137. data/web/assets/javascripts/locales/jquery.timeago.th.js +20 -0
  138. data/web/assets/javascripts/locales/jquery.timeago.tr.js +16 -0
  139. data/web/assets/javascripts/locales/jquery.timeago.uk.js +34 -0
  140. data/web/assets/javascripts/locales/jquery.timeago.uz.js +19 -0
  141. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +20 -0
  142. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +20 -0
  143. data/web/assets/stylesheets/application.css +754 -0
  144. data/web/assets/stylesheets/bootstrap.css +9 -0
  145. data/web/locales/cs.yml +78 -0
  146. data/web/locales/da.yml +68 -0
  147. data/web/locales/de.yml +69 -0
  148. data/web/locales/el.yml +68 -0
  149. data/web/locales/en.yml +79 -0
  150. data/web/locales/es.yml +69 -0
  151. data/web/locales/fr.yml +78 -0
  152. data/web/locales/hi.yml +75 -0
  153. data/web/locales/it.yml +69 -0
  154. data/web/locales/ja.yml +78 -0
  155. data/web/locales/ko.yml +68 -0
  156. data/web/locales/nb.yml +77 -0
  157. data/web/locales/nl.yml +68 -0
  158. data/web/locales/pl.yml +59 -0
  159. data/web/locales/pt-br.yml +68 -0
  160. data/web/locales/pt.yml +67 -0
  161. data/web/locales/ru.yml +78 -0
  162. data/web/locales/sv.yml +68 -0
  163. data/web/locales/ta.yml +75 -0
  164. data/web/locales/uk.yml +76 -0
  165. data/web/locales/zh-cn.yml +68 -0
  166. data/web/locales/zh-tw.yml +68 -0
  167. data/web/views/_footer.erb +17 -0
  168. data/web/views/_job_info.erb +88 -0
  169. data/web/views/_nav.erb +66 -0
  170. data/web/views/_paging.erb +23 -0
  171. data/web/views/_poll_js.erb +5 -0
  172. data/web/views/_poll_link.erb +7 -0
  173. data/web/views/_status.erb +4 -0
  174. data/web/views/_summary.erb +40 -0
  175. data/web/views/busy.erb +94 -0
  176. data/web/views/dashboard.erb +75 -0
  177. data/web/views/dead.erb +34 -0
  178. data/web/views/layout.erb +32 -0
  179. data/web/views/morgue.erb +71 -0
  180. data/web/views/queue.erb +45 -0
  181. data/web/views/queues.erb +28 -0
  182. data/web/views/retries.erb +74 -0
  183. data/web/views/retry.erb +34 -0
  184. data/web/views/scheduled.erb +54 -0
  185. data/web/views/scheduled_job_info.erb +8 -0
  186. metadata +408 -0
@@ -0,0 +1,844 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+ require 'sidekiq'
4
+
5
+ module Sidekiq
6
+ class Stats
7
+ def initialize
8
+ fetch_stats!
9
+ end
10
+
11
+ def processed
12
+ stat :processed
13
+ end
14
+
15
+ def failed
16
+ stat :failed
17
+ end
18
+
19
+ def scheduled_size
20
+ stat :scheduled_size
21
+ end
22
+
23
+ def retry_size
24
+ stat :retry_size
25
+ end
26
+
27
+ def dead_size
28
+ stat :dead_size
29
+ end
30
+
31
+ def enqueued
32
+ stat :enqueued
33
+ end
34
+
35
+ def processes_size
36
+ stat :processes_size
37
+ end
38
+
39
+ def workers_size
40
+ stat :workers_size
41
+ end
42
+
43
+ def default_queue_latency
44
+ stat :default_queue_latency
45
+ end
46
+
47
+ def queues
48
+ Sidekiq::Stats::Queues.new.lengths
49
+ end
50
+
51
+ def fetch_stats!
52
+ pipe1_res = Sidekiq.redis do |conn|
53
+ conn.pipelined do
54
+ conn.get('stat:processed'.freeze)
55
+ conn.get('stat:failed'.freeze)
56
+ conn.zcard('schedule'.freeze)
57
+ conn.zcard('retry'.freeze)
58
+ conn.zcard('dead'.freeze)
59
+ conn.scard('processes'.freeze)
60
+ conn.lrange('queue:default'.freeze, -1, -1)
61
+ conn.smembers('processes'.freeze)
62
+ conn.smembers('queues'.freeze)
63
+ end
64
+ end
65
+
66
+ pipe2_res = Sidekiq.redis do |conn|
67
+ conn.pipelined do
68
+ pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
69
+ pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
70
+ end
71
+ end
72
+
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
+ @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
93
+ }
94
+ end
95
+
96
+ def reset(*stats)
97
+ all = %w(failed processed)
98
+ stats = stats.empty? ? all : all & stats.flatten.compact.map(&:to_s)
99
+
100
+ mset_args = []
101
+ stats.each do |stat|
102
+ mset_args << "stat:#{stat}"
103
+ mset_args << 0
104
+ end
105
+ Sidekiq.redis do |conn|
106
+ conn.mset(*mset_args)
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def stat(s)
113
+ @stats[s]
114
+ end
115
+
116
+ class Queues
117
+ def lengths
118
+ Sidekiq.redis do |conn|
119
+ queues = conn.smembers('queues'.freeze)
120
+
121
+ lengths = conn.pipelined do
122
+ queues.each do |queue|
123
+ conn.llen("queue:#{queue}")
124
+ 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 }
133
+
134
+ Hash[array_of_arrays.reverse]
135
+ end
136
+ end
137
+ end
138
+
139
+ class History
140
+ def initialize(days_previous, start_date = nil)
141
+ @days_previous = days_previous
142
+ @start_date = start_date || Time.now.utc.to_date
143
+ end
144
+
145
+ def processed
146
+ date_stat_hash("processed")
147
+ end
148
+
149
+ def failed
150
+ date_stat_hash("failed")
151
+ end
152
+
153
+ private
154
+
155
+ def date_stat_hash(stat)
156
+ i = 0
157
+ 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
168
+
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
172
+ end
173
+ end
174
+
175
+ stat_hash
176
+ end
177
+ end
178
+ end
179
+
180
+ ##
181
+ # Encapsulates a queue within Sidekiq.
182
+ # Allows enumeration of all jobs within the queue
183
+ # and deletion of jobs.
184
+ #
185
+ # queue = Sidekiq::Queue.new("mailer")
186
+ # queue.each do |job|
187
+ # job.klass # => 'MyWorker'
188
+ # job.args # => [1, 2, 3]
189
+ # job.delete if job.jid == 'abcdef1234567890'
190
+ # end
191
+ #
192
+ class Queue
193
+ include Enumerable
194
+
195
+ ##
196
+ # Return all known queues within Redis.
197
+ #
198
+ def self.all
199
+ Sidekiq.redis { |c| c.smembers('queues'.freeze) }.sort.map { |q| Sidekiq::Queue.new(q) }
200
+ end
201
+
202
+ attr_reader :name
203
+
204
+ def initialize(name="default")
205
+ @name = name
206
+ @rname = "queue:#{name}"
207
+ end
208
+
209
+ def size
210
+ Sidekiq.redis { |con| con.llen(@rname) }
211
+ end
212
+
213
+ # Sidekiq Pro overrides this
214
+ def paused?
215
+ false
216
+ end
217
+
218
+ ##
219
+ # Calculates this queue's latency, the difference in seconds since the oldest
220
+ # job in the queue was enqueued.
221
+ #
222
+ # @return Float
223
+ def latency
224
+ entry = Sidekiq.redis do |conn|
225
+ conn.lrange(@rname, -1, -1)
226
+ end.first
227
+ return 0 unless entry
228
+ Time.now.to_f - Sidekiq.load_json(entry)['enqueued_at']
229
+ end
230
+
231
+ def each
232
+ initial_size = size
233
+ deleted_size = 0
234
+ page = 0
235
+ page_size = 50
236
+
237
+ while true do
238
+ range_start = page * page_size - deleted_size
239
+ range_end = range_start + page_size - 1
240
+ entries = Sidekiq.redis do |conn|
241
+ conn.lrange @rname, range_start, range_end
242
+ end
243
+ break if entries.empty?
244
+ page += 1
245
+ entries.each do |entry|
246
+ yield Job.new(entry, @name)
247
+ end
248
+ deleted_size = initial_size - size
249
+ end
250
+ end
251
+
252
+ ##
253
+ # Find the job with the given JID within this queue.
254
+ #
255
+ # This is a slow, inefficient operation. Do not use under
256
+ # normal conditions. Sidekiq Pro contains a faster version.
257
+ def find_job(jid)
258
+ detect { |j| j.jid == jid }
259
+ end
260
+
261
+ def clear
262
+ Sidekiq.redis do |conn|
263
+ conn.multi do
264
+ conn.del(@rname)
265
+ conn.srem("queues".freeze, name)
266
+ end
267
+ end
268
+ end
269
+ alias_method :💣, :clear
270
+ end
271
+
272
+ ##
273
+ # Encapsulates a pending job within a Sidekiq queue or
274
+ # sorted set.
275
+ #
276
+ # The job should be considered immutable but may be
277
+ # removed from the queue via Job#delete.
278
+ #
279
+ class Job
280
+ attr_reader :item
281
+ attr_reader :value
282
+
283
+ def initialize(item, queue_name=nil)
284
+ @value = item
285
+ @item = item.is_a?(Hash) ? item : Sidekiq.load_json(item)
286
+ @queue = queue_name || @item['queue']
287
+ end
288
+
289
+ def klass
290
+ @item['class']
291
+ end
292
+
293
+ def display_class
294
+ # 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
311
+ end
312
+
313
+ def display_args
314
+ # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
315
+ @args ||= case klass
316
+ when /\ASidekiq::Extensions::Delayed/
317
+ safe_load(args[0], args) do |_, _, arg|
318
+ arg
319
+ end
320
+ 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)
325
+ else
326
+ job_args
327
+ end
328
+ else
329
+ args
330
+ end
331
+ end
332
+
333
+ def args
334
+ @item['args']
335
+ end
336
+
337
+ def jid
338
+ @item['jid']
339
+ end
340
+
341
+ def enqueued_at
342
+ @item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
343
+ end
344
+
345
+ def created_at
346
+ Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
347
+ end
348
+
349
+ def queue
350
+ @queue
351
+ end
352
+
353
+ def latency
354
+ Time.now.to_f - (@item['enqueued_at'] || @item['created_at'])
355
+ end
356
+
357
+ ##
358
+ # Remove this job from the queue.
359
+ def delete
360
+ count = Sidekiq.redis do |conn|
361
+ conn.lrem("queue:#{@queue}", 1, @value)
362
+ end
363
+ count != 0
364
+ end
365
+
366
+ def [](name)
367
+ @item[name]
368
+ end
369
+
370
+ private
371
+
372
+ 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
380
+ end
381
+ end
382
+ end
383
+
384
+ class SortedEntry < Job
385
+ attr_reader :score
386
+ attr_reader :parent
387
+
388
+ def initialize(parent, score, item)
389
+ super(item)
390
+ @score = score
391
+ @parent = parent
392
+ end
393
+
394
+ def at
395
+ Time.at(score).utc
396
+ end
397
+
398
+ def delete
399
+ if @value
400
+ @parent.delete_by_value(@parent.name, @value)
401
+ else
402
+ @parent.delete_by_jid(score, jid)
403
+ end
404
+ end
405
+
406
+ def reschedule(at)
407
+ delete
408
+ @parent.schedule(at, item)
409
+ end
410
+
411
+ def add_to_queue
412
+ remove_job do |message|
413
+ msg = Sidekiq.load_json(message)
414
+ Sidekiq::Client.push(msg)
415
+ end
416
+ end
417
+
418
+ def retry
419
+ raise "Retry not available on jobs which have not failed" unless item["failed_at"]
420
+ remove_job do |message|
421
+ msg = Sidekiq.load_json(message)
422
+ msg['retry_count'] -= 1
423
+ Sidekiq::Client.push(msg)
424
+ end
425
+ end
426
+
427
+ ##
428
+ # Place job in the dead set
429
+ def kill
430
+ raise 'Kill not available on jobs which have not failed' unless item['failed_at']
431
+ 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
441
+ end
442
+ end
443
+
444
+ private
445
+
446
+ def remove_job
447
+ Sidekiq.redis do |conn|
448
+ results = conn.multi do
449
+ conn.zrangebyscore(parent.name, score, score)
450
+ conn.zremrangebyscore(parent.name, score, score)
451
+ end.first
452
+
453
+ if results.size == 1
454
+ yield results.first
455
+ else
456
+ # multiple jobs with the same score
457
+ # find the one with the right JID and push it
458
+ hash = results.group_by do |message|
459
+ if message.index(jid)
460
+ msg = Sidekiq.load_json(message)
461
+ msg['jid'] == jid
462
+ else
463
+ false
464
+ end
465
+ end
466
+
467
+ msg = hash.fetch(true, []).first
468
+ yield msg if msg
469
+
470
+ # push the rest back onto the sorted set
471
+ conn.multi do
472
+ hash.fetch(false, []).each do |message|
473
+ conn.zadd(parent.name, score.to_f.to_s, message)
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
479
+
480
+ end
481
+
482
+ class SortedSet
483
+ include Enumerable
484
+
485
+ attr_reader :name
486
+
487
+ def initialize(name)
488
+ @name = name
489
+ @_size = size
490
+ end
491
+
492
+ def size
493
+ Sidekiq.redis { |c| c.zcard(name) }
494
+ end
495
+
496
+ def clear
497
+ Sidekiq.redis do |conn|
498
+ conn.del(name)
499
+ end
500
+ end
501
+ alias_method :💣, :clear
502
+ end
503
+
504
+ class JobSet < SortedSet
505
+
506
+ def schedule(timestamp, message)
507
+ Sidekiq.redis do |conn|
508
+ conn.zadd(name, timestamp.to_f.to_s, Sidekiq.dump_json(message))
509
+ end
510
+ end
511
+
512
+ def each
513
+ initial_size = @_size
514
+ offset_size = 0
515
+ page = -1
516
+ page_size = 50
517
+
518
+ while true do
519
+ range_start = page * page_size + offset_size
520
+ range_end = range_start + page_size - 1
521
+ elements = Sidekiq.redis do |conn|
522
+ conn.zrange name, range_start, range_end, with_scores: true
523
+ end
524
+ break if elements.empty?
525
+ page -= 1
526
+ elements.each do |element, score|
527
+ yield SortedEntry.new(self, score, element)
528
+ end
529
+ offset_size = initial_size - @_size
530
+ end
531
+ end
532
+
533
+ 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
542
+ else
543
+ result << entry
544
+ end
545
+ result
546
+ end
547
+ end
548
+
549
+ ##
550
+ # 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.
554
+ def find_job(jid)
555
+ self.detect { |j| j.jid == jid }
556
+ end
557
+
558
+ def delete_by_value(name, value)
559
+ Sidekiq.redis do |conn|
560
+ ret = conn.zrem(name, value)
561
+ @_size -= 1 if ret
562
+ ret
563
+ end
564
+ end
565
+
566
+ def delete_by_jid(score, jid)
567
+ Sidekiq.redis do |conn|
568
+ elements = conn.zrangebyscore(name, score, score)
569
+ 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
575
+ end
576
+ false
577
+ end
578
+ end
579
+ end
580
+
581
+ alias_method :delete, :delete_by_jid
582
+ end
583
+
584
+ ##
585
+ # Allows enumeration of scheduled jobs within Sidekiq.
586
+ # Based on this, you can search/filter for jobs. Here's an
587
+ # example where I'm selecting all jobs of a certain type
588
+ # and deleting them from the retry queue.
589
+ #
590
+ # 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'
595
+ # end.map(&:delete)
596
+ class ScheduledSet < JobSet
597
+ def initialize
598
+ super 'schedule'
599
+ end
600
+ end
601
+
602
+ ##
603
+ # Allows enumeration of retries within Sidekiq.
604
+ # Based on this, you can search/filter for jobs. Here's an
605
+ # example where I'm selecting all jobs of a certain type
606
+ # and deleting them from the retry queue.
607
+ #
608
+ # r = Sidekiq::RetrySet.new
609
+ # r.select do |retri|
610
+ # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
611
+ # retri.args[0] == 'User' &&
612
+ # retri.args[1] == 'setup_new_subscriber'
613
+ # end.map(&:delete)
614
+ class RetrySet < JobSet
615
+ def initialize
616
+ super 'retry'
617
+ end
618
+
619
+ def retry_all
620
+ while size > 0
621
+ each(&:retry)
622
+ end
623
+ end
624
+ end
625
+
626
+ ##
627
+ # Allows enumeration of dead jobs within Sidekiq.
628
+ #
629
+ class DeadSet < JobSet
630
+ def initialize
631
+ super 'dead'
632
+ end
633
+
634
+ def retry_all
635
+ while size > 0
636
+ each(&:retry)
637
+ end
638
+ end
639
+
640
+ def self.max_jobs
641
+ Sidekiq.options[:dead_max_jobs]
642
+ end
643
+
644
+ def self.timeout
645
+ Sidekiq.options[:dead_timeout_in_seconds]
646
+ end
647
+ end
648
+
649
+ ##
650
+ # Enumerates the set of Sidekiq processes which are actively working
651
+ # right now. Each process send a heartbeat to Redis every 5 seconds
652
+ # so this set should be relatively accurate, barring network partitions.
653
+ #
654
+ # Yields a Sidekiq::Process.
655
+ #
656
+ class ProcessSet
657
+ include Enumerable
658
+
659
+ def initialize(clean_plz=true)
660
+ self.class.cleanup if clean_plz
661
+ end
662
+
663
+ # Cleans up dead processes recorded in Redis.
664
+ # Returns the number of processes cleaned.
665
+ def self.cleanup
666
+ count = 0
667
+ Sidekiq.redis do |conn|
668
+ procs = conn.smembers('processes').sort
669
+ heartbeats = conn.pipelined do
670
+ procs.each do |key|
671
+ conn.hget(key, 'info')
672
+ end
673
+ end
674
+
675
+ # the hash named key has an expiry of 60 seconds.
676
+ # if it's not found, that means the process has not reported
677
+ # 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?
683
+ end
684
+ count
685
+ end
686
+
687
+ def each
688
+ procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
689
+
690
+ Sidekiq.redis do |conn|
691
+ # We're making a tradeoff here between consuming more memory instead of
692
+ # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
693
+ # you'll be happier this way
694
+ result = conn.pipelined do
695
+ procs.each do |key|
696
+ conn.hmget(key, 'info', 'busy', 'beat', 'quiet')
697
+ end
698
+ end
699
+
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
705
+
706
+ nil
707
+ end
708
+
709
+ # This method is not guaranteed accurate since it does not prune the set
710
+ # based on current heartbeat. #each does that and ensures the set only
711
+ # contains Sidekiq processes which have sent a heartbeat within the last
712
+ # 60 seconds.
713
+ def size
714
+ Sidekiq.redis { |conn| conn.scard('processes') }
715
+ end
716
+ end
717
+
718
+ #
719
+ # Sidekiq::Process represents an active Sidekiq process talking with Redis.
720
+ # Each process has a set of attributes which look like this:
721
+ #
722
+ # {
723
+ # 'hostname' => 'app-1.example.com',
724
+ # 'started_at' => <process start time>,
725
+ # 'pid' => 12345,
726
+ # 'tag' => 'myapp'
727
+ # 'concurrency' => 25,
728
+ # 'queues' => ['default', 'low'],
729
+ # 'busy' => 10,
730
+ # 'beat' => <last heartbeat>,
731
+ # 'identity' => <unique string identifying the process>,
732
+ # }
733
+ class Process
734
+ def initialize(hash)
735
+ @attribs = hash
736
+ end
737
+
738
+ def tag
739
+ self['tag']
740
+ end
741
+
742
+ def labels
743
+ Array(self['labels'])
744
+ end
745
+
746
+ def [](key)
747
+ @attribs[key]
748
+ end
749
+
750
+ def quiet!
751
+ signal('USR1')
752
+ end
753
+
754
+ def stop!
755
+ signal('TERM')
756
+ end
757
+
758
+ def dump_threads
759
+ signal('TTIN')
760
+ end
761
+
762
+ def stopping?
763
+ self['quiet'] == 'true'
764
+ end
765
+
766
+ private
767
+
768
+ def signal(sig)
769
+ key = "#{identity}-signals"
770
+ Sidekiq.redis do |c|
771
+ c.multi do
772
+ c.lpush(key, sig)
773
+ c.expire(key, 60)
774
+ end
775
+ end
776
+ end
777
+
778
+ def identity
779
+ self['identity']
780
+ end
781
+ end
782
+
783
+ ##
784
+ # A worker is a thread that is currently processing a job.
785
+ # Programmatic access to the current active worker set.
786
+ #
787
+ # WARNING WARNING WARNING
788
+ #
789
+ # This is live data that can change every millisecond.
790
+ # If you call #size => 5 and then expect #each to be
791
+ # called 5 times, you're going to have a bad time.
792
+ #
793
+ # workers = Sidekiq::Workers.new
794
+ # workers.size => 2
795
+ # workers.each do |process_id, thread_id, work|
796
+ # # process_id is a unique identifier per Sidekiq process
797
+ # # thread_id is a unique identifier per thread
798
+ # # work is a Hash which looks like:
799
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
800
+ # # run_at is an epoch Integer.
801
+ # end
802
+ #
803
+ class Workers
804
+ include Enumerable
805
+
806
+ def each
807
+ Sidekiq.redis do |conn|
808
+ procs = conn.smembers('processes')
809
+ procs.sort.each do |key|
810
+ valid, workers = conn.pipelined do
811
+ conn.exists(key)
812
+ conn.hgetall("#{key}:workers")
813
+ end
814
+ next unless valid
815
+ workers.each_pair do |tid, json|
816
+ yield key, tid, Sidekiq.load_json(json)
817
+ end
818
+ end
819
+ end
820
+ end
821
+
822
+ # Note that #size is only as accurate as Sidekiq's heartbeat,
823
+ # which happens every 5 seconds. It is NOT real-time.
824
+ #
825
+ # Not very efficient if you have lots of Sidekiq
826
+ # processes but the alternative is a global counter
827
+ # which can easily get out of sync with crashy processes.
828
+ def size
829
+ Sidekiq.redis do |conn|
830
+ procs = conn.smembers('processes')
831
+ if procs.empty?
832
+ 0
833
+ else
834
+ conn.pipelined do
835
+ procs.each do |key|
836
+ conn.hget(key, 'busy')
837
+ end
838
+ end.map(&:to_i).inject(:+)
839
+ end
840
+ end
841
+ end
842
+ end
843
+
844
+ end