sr-sidekiq 4.1.6

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 (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