sidekiq 6.0.1 → 6.2.2

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +147 -2
  3. data/LICENSE +1 -1
  4. data/README.md +4 -7
  5. data/bin/sidekiq +26 -2
  6. data/lib/generators/sidekiq/worker_generator.rb +1 -1
  7. data/lib/sidekiq/api.rb +151 -111
  8. data/lib/sidekiq/cli.rb +39 -10
  9. data/lib/sidekiq/client.rb +26 -15
  10. data/lib/sidekiq/extensions/action_mailer.rb +3 -2
  11. data/lib/sidekiq/extensions/active_record.rb +4 -3
  12. data/lib/sidekiq/extensions/class_methods.rb +5 -4
  13. data/lib/sidekiq/extensions/generic_proxy.rb +3 -1
  14. data/lib/sidekiq/fetch.rb +29 -21
  15. data/lib/sidekiq/job.rb +8 -0
  16. data/lib/sidekiq/job_logger.rb +2 -2
  17. data/lib/sidekiq/job_retry.rb +11 -12
  18. data/lib/sidekiq/launcher.rb +104 -24
  19. data/lib/sidekiq/logger.rb +12 -11
  20. data/lib/sidekiq/manager.rb +4 -4
  21. data/lib/sidekiq/middleware/chain.rb +6 -4
  22. data/lib/sidekiq/monitor.rb +2 -17
  23. data/lib/sidekiq/processor.rb +17 -39
  24. data/lib/sidekiq/rails.rb +16 -18
  25. data/lib/sidekiq/redis_connection.rb +21 -13
  26. data/lib/sidekiq/scheduled.rb +7 -1
  27. data/lib/sidekiq/sd_notify.rb +149 -0
  28. data/lib/sidekiq/systemd.rb +24 -0
  29. data/lib/sidekiq/testing.rb +2 -4
  30. data/lib/sidekiq/util.rb +28 -2
  31. data/lib/sidekiq/version.rb +1 -1
  32. data/lib/sidekiq/web/action.rb +2 -2
  33. data/lib/sidekiq/web/application.rb +30 -19
  34. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  35. data/lib/sidekiq/web/helpers.rb +35 -24
  36. data/lib/sidekiq/web/router.rb +6 -5
  37. data/lib/sidekiq/web.rb +37 -73
  38. data/lib/sidekiq/worker.rb +4 -7
  39. data/lib/sidekiq.rb +14 -8
  40. data/sidekiq.gemspec +12 -5
  41. data/web/assets/images/apple-touch-icon.png +0 -0
  42. data/web/assets/javascripts/application.js +25 -27
  43. data/web/assets/stylesheets/application-dark.css +146 -124
  44. data/web/assets/stylesheets/application.css +35 -135
  45. data/web/locales/ar.yml +8 -2
  46. data/web/locales/de.yml +14 -2
  47. data/web/locales/en.yml +5 -0
  48. data/web/locales/es.yml +18 -2
  49. data/web/locales/fr.yml +10 -3
  50. data/web/locales/ja.yml +5 -0
  51. data/web/locales/lt.yml +83 -0
  52. data/web/locales/pl.yml +4 -4
  53. data/web/locales/ru.yml +4 -0
  54. data/web/locales/vi.yml +83 -0
  55. data/web/views/_job_info.erb +1 -1
  56. data/web/views/busy.erb +50 -19
  57. data/web/views/dashboard.erb +14 -6
  58. data/web/views/dead.erb +1 -1
  59. data/web/views/layout.erb +2 -1
  60. data/web/views/morgue.erb +6 -6
  61. data/web/views/queue.erb +1 -1
  62. data/web/views/queues.erb +10 -2
  63. data/web/views/retries.erb +7 -7
  64. data/web/views/retry.erb +1 -1
  65. data/web/views/scheduled.erb +1 -1
  66. metadata +26 -50
  67. data/.circleci/config.yml +0 -82
  68. data/.github/contributing.md +0 -32
  69. data/.github/issue_template.md +0 -11
  70. data/.gitignore +0 -13
  71. data/.standard.yml +0 -20
  72. data/3.0-Upgrade.md +0 -70
  73. data/4.0-Upgrade.md +0 -53
  74. data/5.0-Upgrade.md +0 -56
  75. data/6.0-Upgrade.md +0 -72
  76. data/COMM-LICENSE +0 -97
  77. data/Ent-2.0-Upgrade.md +0 -37
  78. data/Ent-Changes.md +0 -256
  79. data/Gemfile +0 -24
  80. data/Gemfile.lock +0 -196
  81. data/Pro-2.0-Upgrade.md +0 -138
  82. data/Pro-3.0-Upgrade.md +0 -44
  83. data/Pro-4.0-Upgrade.md +0 -35
  84. data/Pro-5.0-Upgrade.md +0 -25
  85. data/Pro-Changes.md +0 -776
  86. data/Rakefile +0 -10
  87. data/code_of_conduct.md +0 -50
data/lib/sidekiq/api.rb CHANGED
@@ -8,7 +8,7 @@ require "base64"
8
8
  module Sidekiq
9
9
  class Stats
10
10
  def initialize
11
- fetch_stats!
11
+ fetch_stats_fast!
12
12
  end
13
13
 
14
14
  def processed
@@ -51,7 +51,8 @@ module Sidekiq
51
51
  Sidekiq::Stats::Queues.new.lengths
52
52
  end
53
53
 
54
- def fetch_stats!
54
+ # O(1) redis calls
55
+ def fetch_stats_fast!
55
56
  pipe1_res = Sidekiq.redis { |conn|
56
57
  conn.pipelined do
57
58
  conn.get("stat:processed")
@@ -64,6 +65,33 @@ module Sidekiq
64
65
  end
65
66
  }
66
67
 
68
+ default_queue_latency = if (entry = pipe1_res[6].first)
69
+ job = begin
70
+ Sidekiq.load_json(entry)
71
+ rescue
72
+ {}
73
+ end
74
+ now = Time.now.to_f
75
+ thence = job["enqueued_at"] || now
76
+ now - thence
77
+ else
78
+ 0
79
+ end
80
+
81
+ @stats = {
82
+ processed: pipe1_res[0].to_i,
83
+ failed: pipe1_res[1].to_i,
84
+ scheduled_size: pipe1_res[2],
85
+ retry_size: pipe1_res[3],
86
+ dead_size: pipe1_res[4],
87
+ processes_size: pipe1_res[5],
88
+
89
+ default_queue_latency: default_queue_latency
90
+ }
91
+ end
92
+
93
+ # O(number of processes + number of queues) redis calls
94
+ def fetch_stats_slow!
67
95
  processes = Sidekiq.redis { |conn|
68
96
  conn.sscan_each("processes").to_a
69
97
  }
@@ -80,33 +108,16 @@ module Sidekiq
80
108
  }
81
109
 
82
110
  s = processes.size
83
- workers_size = pipe2_res[0...s].map(&:to_i).inject(0, &:+)
84
- enqueued = pipe2_res[s..-1].map(&:to_i).inject(0, &:+)
111
+ workers_size = pipe2_res[0...s].sum(&:to_i)
112
+ enqueued = pipe2_res[s..-1].sum(&:to_i)
85
113
 
86
- default_queue_latency = if (entry = pipe1_res[6].first)
87
- job = begin
88
- Sidekiq.load_json(entry)
89
- rescue
90
- {}
91
- end
92
- now = Time.now.to_f
93
- thence = job["enqueued_at"] || now
94
- now - thence
95
- else
96
- 0
97
- end
98
- @stats = {
99
- processed: pipe1_res[0].to_i,
100
- failed: pipe1_res[1].to_i,
101
- scheduled_size: pipe1_res[2],
102
- retry_size: pipe1_res[3],
103
- dead_size: pipe1_res[4],
104
- processes_size: pipe1_res[5],
114
+ @stats[:workers_size] = workers_size
115
+ @stats[:enqueued] = enqueued
116
+ end
105
117
 
106
- default_queue_latency: default_queue_latency,
107
- workers_size: workers_size,
108
- enqueued: enqueued,
109
- }
118
+ def fetch_stats!
119
+ fetch_stats_fast!
120
+ fetch_stats_slow!
110
121
  end
111
122
 
112
123
  def reset(*stats)
@@ -126,7 +137,8 @@ module Sidekiq
126
137
  private
127
138
 
128
139
  def stat(s)
129
- @stats[s]
140
+ fetch_stats_slow! if @stats[s].nil?
141
+ @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
130
142
  end
131
143
 
132
144
  class Queues
@@ -140,13 +152,8 @@ module Sidekiq
140
152
  end
141
153
  }
142
154
 
143
- i = 0
144
- array_of_arrays = queues.each_with_object({}) { |queue, memo|
145
- memo[queue] = lengths[i]
146
- i += 1
147
- }.sort_by { |_, size| size }
148
-
149
- Hash[array_of_arrays.reverse]
155
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
156
+ array_of_arrays.to_h
150
157
  end
151
158
  end
152
159
  end
@@ -168,18 +175,12 @@ module Sidekiq
168
175
  private
169
176
 
170
177
  def date_stat_hash(stat)
171
- i = 0
172
178
  stat_hash = {}
173
- keys = []
174
- dates = []
175
-
176
- while i < @days_previous
177
- date = @start_date - i
178
- datestr = date.strftime("%Y-%m-%d")
179
- keys << "stat:#{stat}:#{datestr}"
180
- dates << datestr
181
- i += 1
182
- end
179
+ dates = @start_date.downto(@start_date - @days_previous + 1).map { |date|
180
+ date.strftime("%Y-%m-%d")
181
+ }
182
+
183
+ keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
183
184
 
184
185
  begin
185
186
  Sidekiq.redis do |conn|
@@ -266,7 +267,7 @@ module Sidekiq
266
267
  break if entries.empty?
267
268
  page += 1
268
269
  entries.each do |entry|
269
- yield Job.new(entry, @name)
270
+ yield JobRecord.new(entry, @name)
270
271
  end
271
272
  deleted_size = initial_size - size
272
273
  end
@@ -276,7 +277,7 @@ module Sidekiq
276
277
  # Find the job with the given JID within this queue.
277
278
  #
278
279
  # This is a slow, inefficient operation. Do not use under
279
- # normal conditions. Sidekiq Pro contains a faster version.
280
+ # normal conditions.
280
281
  def find_job(jid)
281
282
  detect { |j| j.jid == jid }
282
283
  end
@@ -284,7 +285,7 @@ module Sidekiq
284
285
  def clear
285
286
  Sidekiq.redis do |conn|
286
287
  conn.multi do
287
- conn.del(@rname)
288
+ conn.unlink(@rname)
288
289
  conn.srem("queues", name)
289
290
  end
290
291
  end
@@ -297,9 +298,9 @@ module Sidekiq
297
298
  # sorted set.
298
299
  #
299
300
  # The job should be considered immutable but may be
300
- # removed from the queue via Job#delete.
301
+ # removed from the queue via JobRecord#delete.
301
302
  #
302
- class Job
303
+ class JobRecord
303
304
  attr_reader :item
304
305
  attr_reader :value
305
306
 
@@ -327,21 +328,23 @@ module Sidekiq
327
328
 
328
329
  def display_class
329
330
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
330
- @klass ||= case klass
331
- when /\ASidekiq::Extensions::Delayed/
332
- safe_load(args[0], klass) do |target, method, _|
333
- "#{target}.#{method}"
334
- end
335
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
336
- job_class = @item["wrapped"] || args[0]
337
- if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
338
- # MailerClass#mailer_method
339
- args[0]["arguments"][0..1].join("#")
340
- else
341
- job_class
342
- end
343
- else
344
- klass
331
+ @klass ||= self["display_class"] || begin
332
+ case klass
333
+ when /\ASidekiq::Extensions::Delayed/
334
+ safe_load(args[0], klass) do |target, method, _|
335
+ "#{target}.#{method}"
336
+ end
337
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
338
+ job_class = @item["wrapped"] || args[0]
339
+ if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
340
+ # MailerClass#mailer_method
341
+ args[0]["arguments"][0..1].join("#")
342
+ else
343
+ job_class
344
+ end
345
+ else
346
+ klass
347
+ end
345
348
  end
346
349
  end
347
350
 
@@ -438,17 +441,23 @@ module Sidekiq
438
441
 
439
442
  def uncompress_backtrace(backtrace)
440
443
  if backtrace.is_a?(Array)
441
- # Handle old jobs with previous backtrace format
444
+ # Handle old jobs with raw Array backtrace format
442
445
  backtrace
443
446
  else
444
447
  decoded = Base64.decode64(backtrace)
445
448
  uncompressed = Zlib::Inflate.inflate(decoded)
446
- Marshal.load(uncompressed)
449
+ begin
450
+ Sidekiq.load_json(uncompressed)
451
+ rescue
452
+ # Handle old jobs with marshalled backtrace format
453
+ # TODO Remove in 7.x
454
+ Marshal.load(uncompressed)
455
+ end
447
456
  end
448
457
  end
449
458
  end
450
459
 
451
- class SortedEntry < Job
460
+ class SortedEntry < JobRecord
452
461
  attr_reader :score
453
462
  attr_reader :parent
454
463
 
@@ -471,8 +480,9 @@ module Sidekiq
471
480
  end
472
481
 
473
482
  def reschedule(at)
474
- delete
475
- @parent.schedule(at, item)
483
+ Sidekiq.redis do |conn|
484
+ conn.zincrby(@parent.name, at.to_f - @score, Sidekiq.dump_json(@item))
485
+ end
476
486
  end
477
487
 
478
488
  def add_to_queue
@@ -516,7 +526,7 @@ module Sidekiq
516
526
  else
517
527
  # multiple jobs with the same score
518
528
  # find the one with the right JID and push it
519
- hash = results.group_by { |message|
529
+ matched, nonmatched = results.partition { |message|
520
530
  if message.index(jid)
521
531
  msg = Sidekiq.load_json(message)
522
532
  msg["jid"] == jid
@@ -525,12 +535,12 @@ module Sidekiq
525
535
  end
526
536
  }
527
537
 
528
- msg = hash.fetch(true, []).first
538
+ msg = matched.first
529
539
  yield msg if msg
530
540
 
531
541
  # push the rest back onto the sorted set
532
542
  conn.multi do
533
- hash.fetch(false, []).each do |message|
543
+ nonmatched.each do |message|
534
544
  conn.zadd(parent.name, score.to_f.to_s, message)
535
545
  end
536
546
  end
@@ -554,7 +564,7 @@ module Sidekiq
554
564
  end
555
565
 
556
566
  def scan(match, count = 100)
557
- return to_enum(:scan, match) unless block_given?
567
+ return to_enum(:scan, match, count) unless block_given?
558
568
 
559
569
  match = "*#{match}*" unless match.include?("*")
560
570
  Sidekiq.redis do |conn|
@@ -566,7 +576,7 @@ module Sidekiq
566
576
 
567
577
  def clear
568
578
  Sidekiq.redis do |conn|
569
- conn.del(name)
579
+ conn.unlink(name)
570
580
  end
571
581
  end
572
582
  alias_method :💣, :clear
@@ -648,11 +658,13 @@ module Sidekiq
648
658
  Sidekiq.redis do |conn|
649
659
  elements = conn.zrangebyscore(name, score, score)
650
660
  elements.each do |element|
651
- message = Sidekiq.load_json(element)
652
- if message["jid"] == jid
653
- ret = conn.zrem(name, element)
654
- @_size -= 1 if ret
655
- break ret
661
+ if element.index(jid)
662
+ message = Sidekiq.load_json(element)
663
+ if message["jid"] == jid
664
+ ret = conn.zrem(name, element)
665
+ @_size -= 1 if ret
666
+ break ret
667
+ end
656
668
  end
657
669
  end
658
670
  end
@@ -776,40 +788,41 @@ module Sidekiq
776
788
  # the hash named key has an expiry of 60 seconds.
777
789
  # if it's not found, that means the process has not reported
778
790
  # in to Redis and probably died.
779
- to_prune = []
780
- heartbeats.each_with_index do |beat, i|
781
- to_prune << procs[i] if beat.nil?
782
- end
791
+ to_prune = procs.select.with_index { |proc, i|
792
+ heartbeats[i].nil?
793
+ }
783
794
  count = conn.srem("processes", to_prune) unless to_prune.empty?
784
795
  end
785
796
  count
786
797
  end
787
798
 
788
799
  def each
789
- procs = Sidekiq.redis { |conn| conn.sscan_each("processes").to_a }.sort
800
+ result = Sidekiq.redis { |conn|
801
+ procs = conn.sscan_each("processes").to_a.sort
790
802
 
791
- Sidekiq.redis do |conn|
792
803
  # We're making a tradeoff here between consuming more memory instead of
793
804
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
794
805
  # you'll be happier this way
795
- result = conn.pipelined {
806
+ conn.pipelined do
796
807
  procs.each do |key|
797
- conn.hmget(key, "info", "busy", "beat", "quiet")
808
+ conn.hmget(key, "info", "busy", "beat", "quiet", "rss", "rtt_us")
798
809
  end
799
- }
810
+ end
811
+ }
800
812
 
801
- result.each do |info, busy, at_s, quiet|
802
- # If a process is stopped between when we query Redis for `procs` and
803
- # when we query for `result`, we will have an item in `result` that is
804
- # composed of `nil` values.
805
- next if info.nil?
813
+ result.each do |info, busy, at_s, quiet, rss, rtt|
814
+ # If a process is stopped between when we query Redis for `procs` and
815
+ # when we query for `result`, we will have an item in `result` that is
816
+ # composed of `nil` values.
817
+ next if info.nil?
806
818
 
807
- hash = Sidekiq.load_json(info)
808
- yield Process.new(hash.merge("busy" => busy.to_i, "beat" => at_s.to_f, "quiet" => quiet))
809
- end
819
+ hash = Sidekiq.load_json(info)
820
+ yield Process.new(hash.merge("busy" => busy.to_i,
821
+ "beat" => at_s.to_f,
822
+ "quiet" => quiet,
823
+ "rss" => rss.to_i,
824
+ "rtt_us" => rtt.to_i))
810
825
  end
811
-
812
- nil
813
826
  end
814
827
 
815
828
  # This method is not guaranteed accurate since it does not prune the set
@@ -820,6 +833,18 @@ module Sidekiq
820
833
  Sidekiq.redis { |conn| conn.scard("processes") }
821
834
  end
822
835
 
836
+ # Total number of threads available to execute jobs.
837
+ # For Sidekiq Enterprise customers this number (in production) must be
838
+ # less than or equal to your licensed concurrency.
839
+ def total_concurrency
840
+ sum { |x| x["concurrency"].to_i }
841
+ end
842
+
843
+ def total_rss_in_kb
844
+ sum { |x| x["rss"].to_i }
845
+ end
846
+ alias_method :total_rss, :total_rss_in_kb
847
+
823
848
  # Returns the identity of the current cluster leader or "" if no leader.
824
849
  # This is a Sidekiq Enterprise feature, will always return "" in Sidekiq
825
850
  # or Sidekiq Pro.
@@ -869,6 +894,10 @@ module Sidekiq
869
894
  self["identity"]
870
895
  end
871
896
 
897
+ def queues
898
+ self["queues"]
899
+ end
900
+
872
901
  def quiet!
873
902
  signal("TSTP")
874
903
  end
@@ -899,8 +928,8 @@ module Sidekiq
899
928
  end
900
929
 
901
930
  ##
902
- # A worker is a thread that is currently processing a job.
903
- # Programmatic access to the current active worker set.
931
+ # The WorkSet stores the work being done by this Sidekiq cluster.
932
+ # It tracks the process and thread working on each job.
904
933
  #
905
934
  # WARNING WARNING WARNING
906
935
  #
@@ -908,33 +937,40 @@ module Sidekiq
908
937
  # If you call #size => 5 and then expect #each to be
909
938
  # called 5 times, you're going to have a bad time.
910
939
  #
911
- # workers = Sidekiq::Workers.new
912
- # workers.size => 2
913
- # workers.each do |process_id, thread_id, work|
940
+ # works = Sidekiq::WorkSet.new
941
+ # works.size => 2
942
+ # works.each do |process_id, thread_id, work|
914
943
  # # process_id is a unique identifier per Sidekiq process
915
944
  # # thread_id is a unique identifier per thread
916
945
  # # work is a Hash which looks like:
917
- # # { 'queue' => name, 'run_at' => timestamp, 'payload' => msg }
946
+ # # { 'queue' => name, 'run_at' => timestamp, 'payload' => job_hash }
918
947
  # # run_at is an epoch Integer.
919
948
  # end
920
949
  #
921
- class Workers
950
+ class WorkSet
922
951
  include Enumerable
923
952
 
924
- def each
953
+ def each(&block)
954
+ results = []
925
955
  Sidekiq.redis do |conn|
926
956
  procs = conn.sscan_each("processes").to_a
927
957
  procs.sort.each do |key|
928
958
  valid, workers = conn.pipelined {
929
- conn.exists(key)
959
+ conn.exists?(key)
930
960
  conn.hgetall("#{key}:workers")
931
961
  }
932
962
  next unless valid
933
963
  workers.each_pair do |tid, json|
934
- yield key, tid, Sidekiq.load_json(json)
964
+ hsh = Sidekiq.load_json(json)
965
+ p = hsh["payload"]
966
+ # avoid breaking API, this is a side effect of the JSON optimization in #4316
967
+ hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
968
+ results << [key, tid, hsh]
935
969
  end
936
970
  end
937
971
  end
972
+
973
+ results.sort_by { |(_, _, hsh)| hsh["run_at"] }.each(&block)
938
974
  end
939
975
 
940
976
  # Note that #size is only as accurate as Sidekiq's heartbeat,
@@ -953,9 +989,13 @@ module Sidekiq
953
989
  procs.each do |key|
954
990
  conn.hget(key, "busy")
955
991
  end
956
- }.map(&:to_i).inject(:+)
992
+ }.sum(&:to_i)
957
993
  end
958
994
  end
959
995
  end
960
996
  end
997
+ # Since "worker" is a nebulous term, we've deprecated the use of this class name.
998
+ # Is "worker" a process, a type of job, a thread? Undefined!
999
+ # WorkSet better describes the data.
1000
+ Workers = WorkSet
961
1001
  end
data/lib/sidekiq/cli.rb CHANGED
@@ -33,14 +33,18 @@ module Sidekiq
33
33
  # Code within this method is not tested because it alters
34
34
  # global process state irreversibly. PRs which improve the
35
35
  # test coverage of Sidekiq::CLI are welcomed.
36
- def run
37
- boot_system
36
+ def run(boot_app: true)
37
+ boot_application if boot_app
38
+
38
39
  if environment == "development" && $stdout.tty? && Sidekiq.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
39
40
  print_banner
40
41
  end
42
+ logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
41
43
 
42
44
  self_read, self_write = IO.pipe
43
45
  sigs = %w[INT TERM TTIN TSTP]
46
+ # USR1 and USR2 don't work on the JVM
47
+ sigs << "USR2" if Sidekiq.pro? && !jruby?
44
48
  sigs.each do |sig|
45
49
  trap sig do
46
50
  self_write.puts(sig)
@@ -51,12 +55,25 @@ module Sidekiq
51
55
 
52
56
  logger.info "Running in #{RUBY_DESCRIPTION}"
53
57
  logger.info Sidekiq::LICENSE
54
- logger.info "Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org" unless defined?(::Sidekiq::Pro)
58
+ logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
55
59
 
56
60
  # touch the connection pool so it is created before we
57
61
  # fire startup and start multithreading.
58
- ver = Sidekiq.redis_info["redis_version"]
59
- raise "You are using Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
62
+ info = Sidekiq.redis_info
63
+ ver = info["redis_version"]
64
+ raise "You are connecting to Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
65
+
66
+ maxmemory_policy = info["maxmemory_policy"]
67
+ if maxmemory_policy != "noeviction"
68
+ logger.warn <<~EOM
69
+
70
+
71
+ WARNING: Your Redis instance will evict Sidekiq data under heavy load.
72
+ The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
73
+ See: https://github.com/mperham/sidekiq/wiki/Using-Redis#memory
74
+
75
+ EOM
76
+ end
60
77
 
61
78
  # Since the user can pass us a connection pool explicitly in the initializer, we
62
79
  # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
@@ -160,7 +177,7 @@ module Sidekiq
160
177
  Sidekiq.logger.warn "<no backtrace available>"
161
178
  end
162
179
  end
163
- },
180
+ }
164
181
  }
165
182
  UNHANDLED_SIGNAL_HANDLER = ->(cli) { Sidekiq.logger.info "No signal handler registered, ignoring" }
166
183
  SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
@@ -179,7 +196,11 @@ module Sidekiq
179
196
  end
180
197
 
181
198
  def set_environment(cli_env)
182
- @environment = cli_env || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
199
+ # See #984 for discussion.
200
+ # APP_ENV is now the preferred ENV term since it is not tech-specific.
201
+ # Both Sinatra 2.0+ and Sidekiq support this term.
202
+ # RAILS_ENV and RACK_ENV are there for legacy support.
203
+ @environment = cli_env || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
183
204
  end
184
205
 
185
206
  def symbolize_keys_deep!(hash)
@@ -221,8 +242,7 @@ module Sidekiq
221
242
  opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
222
243
 
223
244
  # set defaults
224
- opts[:queues] = ["default"] if opts[:queues].nil? || opts[:queues].empty?
225
- opts[:strict] = true if opts[:strict].nil?
245
+ opts[:queues] = ["default"] if opts[:queues].nil?
226
246
  opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
227
247
 
228
248
  # merge with defaults
@@ -233,7 +253,7 @@ module Sidekiq
233
253
  Sidekiq.options
234
254
  end
235
255
 
236
- def boot_system
256
+ def boot_application
237
257
  ENV["RACK_ENV"] = ENV["RAILS_ENV"] = environment
238
258
 
239
259
  if File.directory?(options[:require])
@@ -361,6 +381,8 @@ module Sidekiq
361
381
  end
362
382
 
363
383
  opts = opts.merge(opts.delete(environment.to_sym) || {})
384
+ opts.delete(:strict)
385
+
364
386
  parse_queues(opts, opts.delete(:queues) || [])
365
387
 
366
388
  opts
@@ -372,9 +394,16 @@ module Sidekiq
372
394
 
373
395
  def parse_queue(opts, queue, weight = nil)
374
396
  opts[:queues] ||= []
397
+ opts[:strict] = true if opts[:strict].nil?
375
398
  raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
376
399
  [weight.to_i, 1].max.times { opts[:queues] << queue }
377
400
  opts[:strict] = false if weight.to_i > 0
378
401
  end
402
+
403
+ def rails_app?
404
+ defined?(::Rails) && ::Rails.respond_to?(:application)
405
+ end
379
406
  end
380
407
  end
408
+
409
+ require "sidekiq/systemd"
@@ -19,7 +19,7 @@ module Sidekiq
19
19
  #
20
20
  def middleware(&block)
21
21
  @chain ||= Sidekiq.client_middleware
22
- if block_given?
22
+ if block
23
23
  @chain = @chain.dup
24
24
  yield @chain
25
25
  end
@@ -90,17 +90,18 @@ module Sidekiq
90
90
  # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
91
91
  # than the number given if the middleware stopped processing for one or more jobs.
92
92
  def push_bulk(items)
93
- arg = items["args"].first
94
- return [] unless arg # no jobs to push
95
- raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless arg.is_a?(Array)
93
+ args = items["args"]
94
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless args.is_a?(Array) && args.all?(Array)
95
+ return [] if args.empty? # no jobs to push
96
96
 
97
97
  at = items.delete("at")
98
98
  raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all?(Numeric))
99
+ raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
99
100
 
100
101
  normed = normalize_item(items)
101
- payloads = items["args"].map.with_index { |args, index|
102
- single_at = at.is_a?(Array) ? at[index] : at
103
- copy = normed.merge("args" => args, "jid" => SecureRandom.hex(12), "at" => single_at, "enqueued_at" => Time.now.to_f)
102
+ payloads = args.map.with_index { |job_args, index|
103
+ copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f)
104
+ copy["at"] = (at.is_a?(Array) ? at[index] : at) if at
104
105
 
105
106
  result = process_single(items["class"], copy)
106
107
  result || nil
@@ -193,7 +194,7 @@ module Sidekiq
193
194
  end
194
195
 
195
196
  def atomic_push(conn, payloads)
196
- if payloads.first["at"]
197
+ if payloads.first.key?("at")
197
198
  conn.zadd("schedule", payloads.map { |hash|
198
199
  at = hash.delete("at").to_s
199
200
  [at, Sidekiq.dump_json(hash)]
@@ -218,21 +219,31 @@ module Sidekiq
218
219
  end
219
220
  end
220
221
 
222
+ def validate(item)
223
+ raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
224
+ raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
225
+ raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
226
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
227
+ raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
228
+ end
229
+
221
230
  def normalize_item(item)
222
- raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
223
- raise(ArgumentError, "Job args must be an Array") unless item["args"].is_a?(Array)
224
- raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
225
- raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.key?("at") && !item["at"].is_a?(Numeric)
226
- raise(ArgumentError, "Job tags must be an Array") if item["tags"] && !item["tags"].is_a?(Array)
231
+ validate(item)
227
232
  # raise(ArgumentError, "Arguments must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices") unless JSON.load(JSON.dump(item['args'])) == item['args']
228
233
 
229
- normalized_hash(item["class"])
230
- .each { |key, value| item[key] = value if item[key].nil? }
234
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
235
+ # this allows ActiveJobs to control sidekiq_options too.
236
+ defaults = normalized_hash(item["class"])
237
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?("get_sidekiq_options")
238
+ item = defaults.merge(item)
239
+
240
+ raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
231
241
 
232
242
  item["class"] = item["class"].to_s
233
243
  item["queue"] = item["queue"].to_s
234
244
  item["jid"] ||= SecureRandom.hex(12)
235
245
  item["created_at"] ||= Time.now.to_f
246
+
236
247
  item
237
248
  end
238
249