sidekiq 4.2.10 → 5.2.10

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 (75) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +61 -0
  3. data/.github/issue_template.md +3 -1
  4. data/.gitignore +3 -0
  5. data/.travis.yml +6 -13
  6. data/5.0-Upgrade.md +56 -0
  7. data/COMM-LICENSE +12 -10
  8. data/Changes.md +177 -1
  9. data/Ent-Changes.md +67 -2
  10. data/Gemfile +12 -22
  11. data/LICENSE +1 -1
  12. data/Pro-4.0-Upgrade.md +35 -0
  13. data/Pro-Changes.md +133 -2
  14. data/README.md +8 -6
  15. data/Rakefile +2 -5
  16. data/bin/sidekiqctl +13 -92
  17. data/bin/sidekiqload +5 -10
  18. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +1 -1
  19. data/lib/sidekiq/api.rb +148 -58
  20. data/lib/sidekiq/cli.rb +120 -81
  21. data/lib/sidekiq/client.rb +25 -18
  22. data/lib/sidekiq/core_ext.rb +1 -119
  23. data/lib/sidekiq/ctl.rb +221 -0
  24. data/lib/sidekiq/delay.rb +42 -0
  25. data/lib/sidekiq/exception_handler.rb +2 -4
  26. data/lib/sidekiq/extensions/generic_proxy.rb +7 -1
  27. data/lib/sidekiq/fetch.rb +1 -1
  28. data/lib/sidekiq/job_logger.rb +25 -0
  29. data/lib/sidekiq/job_retry.rb +262 -0
  30. data/lib/sidekiq/launcher.rb +20 -20
  31. data/lib/sidekiq/logging.rb +18 -2
  32. data/lib/sidekiq/manager.rb +5 -6
  33. data/lib/sidekiq/middleware/server/active_record.rb +10 -0
  34. data/lib/sidekiq/processor.rb +126 -48
  35. data/lib/sidekiq/rails.rb +8 -73
  36. data/lib/sidekiq/redis_connection.rb +43 -5
  37. data/lib/sidekiq/scheduled.rb +35 -8
  38. data/lib/sidekiq/testing.rb +16 -7
  39. data/lib/sidekiq/util.rb +5 -2
  40. data/lib/sidekiq/version.rb +1 -1
  41. data/lib/sidekiq/web/action.rb +3 -7
  42. data/lib/sidekiq/web/application.rb +37 -17
  43. data/lib/sidekiq/web/helpers.rb +69 -22
  44. data/lib/sidekiq/web/router.rb +10 -10
  45. data/lib/sidekiq/web.rb +4 -4
  46. data/lib/sidekiq/worker.rb +118 -19
  47. data/lib/sidekiq.rb +27 -27
  48. data/sidekiq.gemspec +6 -17
  49. data/web/assets/javascripts/application.js +0 -0
  50. data/web/assets/javascripts/dashboard.js +32 -17
  51. data/web/assets/stylesheets/application-rtl.css +246 -0
  52. data/web/assets/stylesheets/application.css +371 -6
  53. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  54. data/web/assets/stylesheets/bootstrap.css +2 -2
  55. data/web/locales/ar.yml +81 -0
  56. data/web/locales/en.yml +2 -0
  57. data/web/locales/es.yml +4 -3
  58. data/web/locales/fa.yml +1 -0
  59. data/web/locales/he.yml +79 -0
  60. data/web/locales/ja.yml +5 -3
  61. data/web/locales/ur.yml +80 -0
  62. data/web/views/_footer.erb +5 -2
  63. data/web/views/_nav.erb +4 -18
  64. data/web/views/_paging.erb +1 -1
  65. data/web/views/busy.erb +9 -5
  66. data/web/views/dashboard.erb +1 -1
  67. data/web/views/layout.erb +11 -2
  68. data/web/views/morgue.erb +4 -4
  69. data/web/views/queue.erb +8 -7
  70. data/web/views/queues.erb +2 -0
  71. data/web/views/retries.erb +9 -5
  72. data/web/views/scheduled.erb +2 -2
  73. metadata +30 -159
  74. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  75. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
data/lib/sidekiq/api.rb CHANGED
@@ -1,9 +1,24 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
  require 'sidekiq'
4
3
 
5
4
  module Sidekiq
5
+
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
18
+
6
19
  class Stats
20
+ include RedisScanner
21
+
7
22
  def initialize
8
23
  fetch_stats!
9
24
  end
@@ -51,33 +66,39 @@ module Sidekiq
51
66
  def fetch_stats!
52
67
  pipe1_res = Sidekiq.redis do |conn|
53
68
  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)
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)
63
76
  end
64
77
  end
65
78
 
79
+ processes = Sidekiq.redis do |conn|
80
+ sscan(conn, 'processes')
81
+ end
82
+
83
+ queues = Sidekiq.redis do |conn|
84
+ sscan(conn, 'queues')
85
+ end
86
+
66
87
  pipe2_res = Sidekiq.redis do |conn|
67
88
  conn.pipelined do
68
- pipe1_res[7].each {|key| conn.hget(key, 'busy'.freeze) }
69
- pipe1_res[8].each {|queue| conn.llen("queue:#{queue}") }
89
+ processes.each {|key| conn.hget(key, 'busy') }
90
+ queues.each {|queue| conn.llen("queue:#{queue}") }
70
91
  end
71
92
  end
72
93
 
73
- s = pipe1_res[7].size
94
+ s = processes.size
74
95
  workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
75
96
  enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
76
97
 
77
98
  default_queue_latency = if (entry = pipe1_res[6].first)
78
- job = Sidekiq.load_json(entry)
99
+ job = Sidekiq.load_json(entry) rescue {}
79
100
  now = Time.now.to_f
80
- thence = job['enqueued_at'.freeze] || now
101
+ thence = job['enqueued_at'] || now
81
102
  now - thence
82
103
  else
83
104
  0
@@ -117,9 +138,11 @@ module Sidekiq
117
138
  end
118
139
 
119
140
  class Queues
141
+ include RedisScanner
142
+
120
143
  def lengths
121
144
  Sidekiq.redis do |conn|
122
- queues = conn.smembers('queues'.freeze)
145
+ queues = sscan(conn, 'queues')
123
146
 
124
147
  lengths = conn.pipelined do
125
148
  queues.each do |queue|
@@ -141,16 +164,18 @@ module Sidekiq
141
164
 
142
165
  class History
143
166
  def initialize(days_previous, start_date = nil)
167
+ #we only store five years of data in Redis
168
+ raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
144
169
  @days_previous = days_previous
145
170
  @start_date = start_date || Time.now.utc.to_date
146
171
  end
147
172
 
148
173
  def processed
149
- date_stat_hash("processed")
174
+ @processed ||= date_stat_hash("processed")
150
175
  end
151
176
 
152
177
  def failed
153
- date_stat_hash("failed")
178
+ @failed ||= date_stat_hash("failed")
154
179
  end
155
180
 
156
181
  private
@@ -163,16 +188,21 @@ module Sidekiq
163
188
 
164
189
  while i < @days_previous
165
190
  date = @start_date - i
166
- datestr = date.strftime("%Y-%m-%d".freeze)
191
+ datestr = date.strftime("%Y-%m-%d")
167
192
  keys << "stat:#{stat}:#{datestr}"
168
193
  dates << datestr
169
194
  i += 1
170
195
  end
171
196
 
172
- Sidekiq.redis do |conn|
173
- conn.mget(keys).each_with_index do |value, idx|
174
- stat_hash[dates[idx]] = value ? value.to_i : 0
197
+ begin
198
+ Sidekiq.redis do |conn|
199
+ conn.mget(keys).each_with_index do |value, idx|
200
+ stat_hash[dates[idx]] = value ? value.to_i : 0
201
+ end
175
202
  end
203
+ rescue Redis::CommandError
204
+ # mget will trigger a CROSSSLOT error when run against a Cluster
205
+ # TODO Someone want to add Cluster support?
176
206
  end
177
207
 
178
208
  stat_hash
@@ -194,18 +224,19 @@ module Sidekiq
194
224
  #
195
225
  class Queue
196
226
  include Enumerable
227
+ extend RedisScanner
197
228
 
198
229
  ##
199
230
  # Return all known queues within Redis.
200
231
  #
201
232
  def self.all
202
- Sidekiq.redis { |c| c.smembers('queues'.freeze) }.sort.map { |q| Sidekiq::Queue.new(q) }
233
+ Sidekiq.redis { |c| sscan(c, 'queues') }.sort.map { |q| Sidekiq::Queue.new(q) }
203
234
  end
204
235
 
205
236
  attr_reader :name
206
237
 
207
238
  def initialize(name="default")
208
- @name = name
239
+ @name = name.to_s
209
240
  @rname = "queue:#{name}"
210
241
  end
211
242
 
@@ -268,7 +299,7 @@ module Sidekiq
268
299
  Sidekiq.redis do |conn|
269
300
  conn.multi do
270
301
  conn.del(@rname)
271
- conn.srem("queues".freeze, name)
302
+ conn.srem("queues", name)
272
303
  end
273
304
  end
274
305
  end
@@ -287,13 +318,25 @@ module Sidekiq
287
318
  attr_reader :value
288
319
 
289
320
  def initialize(item, queue_name=nil)
321
+ @args = nil
290
322
  @value = item
291
- @item = item.is_a?(Hash) ? item : Sidekiq.load_json(item)
323
+ @item = item.is_a?(Hash) ? item : parse(item)
292
324
  @queue = queue_name || @item['queue']
293
325
  end
294
326
 
327
+ def parse(item)
328
+ Sidekiq.load_json(item)
329
+ rescue JSON::ParserError
330
+ # If the job payload in Redis is invalid JSON, we'll load
331
+ # the item as an empty hash and store the invalid JSON as
332
+ # the job 'args' for display in the Web UI.
333
+ @invalid = true
334
+ @args = [item]
335
+ {}
336
+ end
337
+
295
338
  def klass
296
- @item['class']
339
+ self['class']
297
340
  end
298
341
 
299
342
  def display_class
@@ -318,38 +361,42 @@ module Sidekiq
318
361
 
319
362
  def display_args
320
363
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
321
- @args ||= case klass
364
+ @display_args ||= case klass
322
365
  when /\ASidekiq::Extensions::Delayed/
323
366
  safe_load(args[0], args) do |_, _, arg|
324
367
  arg
325
368
  end
326
369
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
327
- job_args = @item['wrapped'] ? args[0]["arguments"] : []
328
- if 'ActionMailer::DeliveryJob' == (@item['wrapped'] || args[0])
329
- # remove MailerClass, mailer_method and 'deliver_now'
330
- job_args.drop(3)
370
+ job_args = self['wrapped'] ? args[0]["arguments"] : []
371
+ if 'ActionMailer::DeliveryJob' == (self['wrapped'] || args[0])
372
+ # remove MailerClass, mailer_method and 'deliver_now'
373
+ job_args.drop(3)
331
374
  else
332
- job_args
375
+ job_args
333
376
  end
334
377
  else
378
+ if self['encrypt']
379
+ # no point in showing 150+ bytes of random garbage
380
+ args[-1] = '[encrypted data]'
381
+ end
335
382
  args
336
383
  end
337
384
  end
338
385
 
339
386
  def args
340
- @item['args']
387
+ @args || @item['args']
341
388
  end
342
389
 
343
390
  def jid
344
- @item['jid']
391
+ self['jid']
345
392
  end
346
393
 
347
394
  def enqueued_at
348
- @item['enqueued_at'] ? Time.at(@item['enqueued_at']).utc : nil
395
+ self['enqueued_at'] ? Time.at(self['enqueued_at']).utc : nil
349
396
  end
350
397
 
351
398
  def created_at
352
- Time.at(@item['created_at'] || @item['enqueued_at'] || 0).utc
399
+ Time.at(self['created_at'] || self['enqueued_at'] || 0).utc
353
400
  end
354
401
 
355
402
  def queue
@@ -371,7 +418,10 @@ module Sidekiq
371
418
  end
372
419
 
373
420
  def [](name)
374
- @item[name]
421
+ # nil will happen if the JSON fails to parse.
422
+ # We don't guarantee Sidekiq will work with bad job JSON but we should
423
+ # make a best effort to minimize the damage.
424
+ @item ? @item[name] : nil
375
425
  end
376
426
 
377
427
  private
@@ -434,14 +484,7 @@ module Sidekiq
434
484
  # Place job in the dead set
435
485
  def kill
436
486
  remove_job do |message|
437
- now = Time.now.to_f
438
- Sidekiq.redis do |conn|
439
- conn.multi do
440
- conn.zadd('dead', now, message)
441
- conn.zremrangebyscore('dead', '-inf', now - DeadSet.timeout)
442
- conn.zremrangebyrank('dead', 0, - DeadSet.max_jobs)
443
- end
444
- end
487
+ DeadSet.new.kill(message)
445
488
  end
446
489
  end
447
490
 
@@ -531,7 +574,7 @@ module Sidekiq
531
574
  end
532
575
  break if elements.empty?
533
576
  page -= 1
534
- elements.each do |element, score|
577
+ elements.reverse.each do |element, score|
535
578
  yield SortedEntry.new(self, score, element)
536
579
  end
537
580
  offset_size = initial_size - @_size
@@ -629,6 +672,12 @@ module Sidekiq
629
672
  each(&:retry)
630
673
  end
631
674
  end
675
+
676
+ def kill_all
677
+ while size > 0
678
+ each(&:kill)
679
+ end
680
+ end
632
681
  end
633
682
 
634
683
  ##
@@ -639,6 +688,27 @@ module Sidekiq
639
688
  super 'dead'
640
689
  end
641
690
 
691
+ def kill(message, opts={})
692
+ now = Time.now.to_f
693
+ Sidekiq.redis do |conn|
694
+ conn.multi do
695
+ conn.zadd(name, now.to_s, message)
696
+ conn.zremrangebyscore(name, '-inf', now - self.class.timeout)
697
+ conn.zremrangebyrank(name, 0, - self.class.max_jobs)
698
+ end
699
+ end
700
+
701
+ if opts[:notify_failure] != false
702
+ job = Sidekiq.load_json(message)
703
+ r = RuntimeError.new("Job killed by API")
704
+ r.set_backtrace(caller)
705
+ Sidekiq.death_handlers.each do |handle|
706
+ handle.call(job, r)
707
+ end
708
+ end
709
+ true
710
+ end
711
+
642
712
  def retry_all
643
713
  while size > 0
644
714
  each(&:retry)
@@ -663,17 +733,18 @@ module Sidekiq
663
733
  #
664
734
  class ProcessSet
665
735
  include Enumerable
736
+ include RedisScanner
666
737
 
667
738
  def initialize(clean_plz=true)
668
- self.class.cleanup if clean_plz
739
+ cleanup if clean_plz
669
740
  end
670
741
 
671
742
  # Cleans up dead processes recorded in Redis.
672
743
  # Returns the number of processes cleaned.
673
- def self.cleanup
744
+ def cleanup
674
745
  count = 0
675
746
  Sidekiq.redis do |conn|
676
- procs = conn.smembers('processes').sort
747
+ procs = sscan(conn, 'processes').sort
677
748
  heartbeats = conn.pipelined do
678
749
  procs.each do |key|
679
750
  conn.hget(key, 'info')
@@ -693,7 +764,7 @@ module Sidekiq
693
764
  end
694
765
 
695
766
  def each
696
- procs = Sidekiq.redis { |conn| conn.smembers('processes') }.sort
767
+ procs = Sidekiq.redis { |conn| sscan(conn, 'processes') }.sort
697
768
 
698
769
  Sidekiq.redis do |conn|
699
770
  # We're making a tradeoff here between consuming more memory instead of
@@ -706,6 +777,11 @@ module Sidekiq
706
777
  end
707
778
 
708
779
  result.each do |info, busy, at_s, quiet|
780
+ # If a process is stopped between when we query Redis for `procs` and
781
+ # when we query for `result`, we will have an item in `result` that is
782
+ # composed of `nil` values.
783
+ next if info.nil?
784
+
709
785
  hash = Sidekiq.load_json(info)
710
786
  yield Process.new(hash.merge('busy' => busy.to_i, 'beat' => at_s.to_f, 'quiet' => quiet))
711
787
  end
@@ -721,6 +797,18 @@ module Sidekiq
721
797
  def size
722
798
  Sidekiq.redis { |conn| conn.scard('processes') }
723
799
  end
800
+
801
+ # Returns the identity of the current cluster leader or "" if no leader.
802
+ # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
803
+ # or Sidekiq Pro.
804
+ def leader
805
+ @leader ||= begin
806
+ x = Sidekiq.redis {|c| c.get("dear-leader") }
807
+ # need a non-falsy value so we can memoize
808
+ x = "" unless x
809
+ x
810
+ end
811
+ end
724
812
  end
725
813
 
726
814
  #
@@ -755,8 +843,12 @@ module Sidekiq
755
843
  @attribs[key]
756
844
  end
757
845
 
846
+ def identity
847
+ self['identity']
848
+ end
849
+
758
850
  def quiet!
759
- signal('USR1')
851
+ signal('TSTP')
760
852
  end
761
853
 
762
854
  def stop!
@@ -783,9 +875,6 @@ module Sidekiq
783
875
  end
784
876
  end
785
877
 
786
- def identity
787
- self['identity']
788
- end
789
878
  end
790
879
 
791
880
  ##
@@ -810,13 +899,14 @@ module Sidekiq
810
899
  #
811
900
  class Workers
812
901
  include Enumerable
902
+ include RedisScanner
813
903
 
814
904
  def each
815
905
  Sidekiq.redis do |conn|
816
- procs = conn.smembers('processes')
906
+ procs = sscan(conn, 'processes')
817
907
  procs.sort.each do |key|
818
908
  valid, workers = conn.pipelined do
819
- conn.exists(key)
909
+ conn.exists?(key)
820
910
  conn.hgetall("#{key}:workers")
821
911
  end
822
912
  next unless valid
@@ -835,7 +925,7 @@ module Sidekiq
835
925
  # which can easily get out of sync with crashy processes.
836
926
  def size
837
927
  Sidekiq.redis do |conn|
838
- procs = conn.smembers('processes')
928
+ procs = sscan(conn, 'processes')
839
929
  if procs.empty?
840
930
  0
841
931
  else