sidekiq 6.5.8 → 7.1.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +133 -15
  3. data/README.md +40 -32
  4. data/bin/sidekiq +3 -8
  5. data/bin/sidekiqload +204 -118
  6. data/bin/sidekiqmon +3 -0
  7. data/lib/sidekiq/api.rb +114 -128
  8. data/lib/sidekiq/capsule.rb +127 -0
  9. data/lib/sidekiq/cli.rb +57 -74
  10. data/lib/sidekiq/client.rb +63 -37
  11. data/lib/sidekiq/component.rb +4 -1
  12. data/lib/sidekiq/config.rb +278 -0
  13. data/lib/sidekiq/deploy.rb +62 -0
  14. data/lib/sidekiq/embedded.rb +61 -0
  15. data/lib/sidekiq/fetch.rb +11 -14
  16. data/lib/sidekiq/job.rb +371 -10
  17. data/lib/sidekiq/job_logger.rb +2 -2
  18. data/lib/sidekiq/job_retry.rb +17 -14
  19. data/lib/sidekiq/job_util.rb +49 -15
  20. data/lib/sidekiq/launcher.rb +66 -62
  21. data/lib/sidekiq/logger.rb +1 -26
  22. data/lib/sidekiq/manager.rb +9 -11
  23. data/lib/sidekiq/metrics/query.rb +4 -4
  24. data/lib/sidekiq/metrics/shared.rb +7 -6
  25. data/lib/sidekiq/metrics/tracking.rb +20 -18
  26. data/lib/sidekiq/middleware/chain.rb +19 -18
  27. data/lib/sidekiq/middleware/current_attributes.rb +52 -20
  28. data/lib/sidekiq/monitor.rb +17 -4
  29. data/lib/sidekiq/paginator.rb +3 -3
  30. data/lib/sidekiq/processor.rb +21 -27
  31. data/lib/sidekiq/rails.rb +12 -7
  32. data/lib/sidekiq/redis_client_adapter.rb +11 -69
  33. data/lib/sidekiq/redis_connection.rb +11 -111
  34. data/lib/sidekiq/scheduled.rb +21 -22
  35. data/lib/sidekiq/testing.rb +5 -33
  36. data/lib/sidekiq/transaction_aware_client.rb +4 -5
  37. data/lib/sidekiq/version.rb +2 -1
  38. data/lib/sidekiq/web/application.rb +21 -6
  39. data/lib/sidekiq/web/csrf_protection.rb +1 -1
  40. data/lib/sidekiq/web/helpers.rb +19 -18
  41. data/lib/sidekiq/web.rb +7 -18
  42. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  43. data/lib/sidekiq.rb +76 -274
  44. data/sidekiq.gemspec +12 -10
  45. data/web/assets/javascripts/application.js +18 -0
  46. data/web/assets/javascripts/base-charts.js +106 -0
  47. data/web/assets/javascripts/dashboard-charts.js +166 -0
  48. data/web/assets/javascripts/dashboard.js +3 -223
  49. data/web/assets/javascripts/metrics.js +117 -115
  50. data/web/assets/stylesheets/application-dark.css +4 -0
  51. data/web/assets/stylesheets/application-rtl.css +2 -91
  52. data/web/assets/stylesheets/application.css +23 -298
  53. data/web/locales/ar.yml +70 -70
  54. data/web/locales/cs.yml +62 -62
  55. data/web/locales/da.yml +60 -53
  56. data/web/locales/de.yml +65 -65
  57. data/web/locales/el.yml +2 -7
  58. data/web/locales/en.yml +76 -70
  59. data/web/locales/es.yml +68 -68
  60. data/web/locales/fa.yml +65 -65
  61. data/web/locales/fr.yml +81 -67
  62. data/web/locales/gd.yml +99 -0
  63. data/web/locales/he.yml +65 -64
  64. data/web/locales/hi.yml +59 -59
  65. data/web/locales/it.yml +53 -53
  66. data/web/locales/ja.yml +67 -69
  67. data/web/locales/ko.yml +52 -52
  68. data/web/locales/lt.yml +66 -66
  69. data/web/locales/nb.yml +61 -61
  70. data/web/locales/nl.yml +52 -52
  71. data/web/locales/pl.yml +45 -45
  72. data/web/locales/pt-br.yml +59 -69
  73. data/web/locales/pt.yml +51 -51
  74. data/web/locales/ru.yml +67 -66
  75. data/web/locales/sv.yml +53 -53
  76. data/web/locales/ta.yml +60 -60
  77. data/web/locales/uk.yml +62 -61
  78. data/web/locales/ur.yml +64 -64
  79. data/web/locales/vi.yml +67 -67
  80. data/web/locales/zh-cn.yml +20 -18
  81. data/web/locales/zh-tw.yml +10 -1
  82. data/web/views/_footer.erb +5 -2
  83. data/web/views/_job_info.erb +18 -2
  84. data/web/views/_metrics_period_select.erb +12 -0
  85. data/web/views/_paging.erb +2 -0
  86. data/web/views/_poll_link.erb +1 -1
  87. data/web/views/busy.erb +39 -28
  88. data/web/views/dashboard.erb +36 -5
  89. data/web/views/metrics.erb +33 -20
  90. data/web/views/metrics_for_job.erb +25 -44
  91. data/web/views/morgue.erb +5 -9
  92. data/web/views/queue.erb +10 -14
  93. data/web/views/queues.erb +3 -1
  94. data/web/views/retries.erb +5 -9
  95. data/web/views/scheduled.erb +12 -13
  96. metadata +43 -39
  97. data/lib/sidekiq/delay.rb +0 -43
  98. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  99. data/lib/sidekiq/extensions/active_record.rb +0 -43
  100. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  101. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  102. data/lib/sidekiq/metrics/deploy.rb +0 -47
  103. data/lib/sidekiq/worker.rb +0 -370
  104. data/web/assets/javascripts/graph.js +0 -16
  105. /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/sidekiq/api.rb CHANGED
@@ -6,10 +6,7 @@ require "zlib"
6
6
  require "set"
7
7
  require "base64"
8
8
 
9
- if ENV["SIDEKIQ_METRICS_BETA"]
10
- require "sidekiq/metrics/deploy"
11
- require "sidekiq/metrics/query"
12
- end
9
+ require "sidekiq/metrics/query"
13
10
 
14
11
  #
15
12
  # Sidekiq's Data API provides a Ruby object model on top
@@ -70,7 +67,18 @@ module Sidekiq
70
67
  end
71
68
 
72
69
  def queues
73
- Sidekiq::Stats::Queues.new.lengths
70
+ Sidekiq.redis do |conn|
71
+ queues = conn.sscan("queues").to_a
72
+
73
+ lengths = conn.pipelined { |pipeline|
74
+ queues.each do |queue|
75
+ pipeline.llen("queue:#{queue}")
76
+ end
77
+ }
78
+
79
+ array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
80
+ array_of_arrays.to_h
81
+ end
74
82
  end
75
83
 
76
84
  # O(1) redis calls
@@ -84,11 +92,11 @@ module Sidekiq
84
92
  pipeline.zcard("retry")
85
93
  pipeline.zcard("dead")
86
94
  pipeline.scard("processes")
87
- pipeline.lrange("queue:default", -1, -1)
95
+ pipeline.lindex("queue:default", -1)
88
96
  end
89
97
  }
90
98
 
91
- default_queue_latency = if (entry = pipe1_res[6].first)
99
+ default_queue_latency = if (entry = pipe1_res[6])
92
100
  job = begin
93
101
  Sidekiq.load_json(entry)
94
102
  rescue
@@ -117,11 +125,11 @@ module Sidekiq
117
125
  # @api private
118
126
  def fetch_stats_slow!
119
127
  processes = Sidekiq.redis { |conn|
120
- conn.sscan_each("processes").to_a
128
+ conn.sscan("processes").to_a
121
129
  }
122
130
 
123
131
  queues = Sidekiq.redis { |conn|
124
- conn.sscan_each("queues").to_a
132
+ conn.sscan("queues").to_a
125
133
  }
126
134
 
127
135
  pipe2_res = Sidekiq.redis { |conn|
@@ -133,7 +141,7 @@ module Sidekiq
133
141
 
134
142
  s = processes.size
135
143
  workers_size = pipe2_res[0...s].sum(&:to_i)
136
- enqueued = pipe2_res[s..-1].sum(&:to_i)
144
+ enqueued = pipe2_res[s..].sum(&:to_i)
137
145
 
138
146
  @stats[:workers_size] = workers_size
139
147
  @stats[:enqueued] = enqueued
@@ -168,25 +176,8 @@ module Sidekiq
168
176
  @stats[s] || raise(ArgumentError, "Unknown stat #{s}")
169
177
  end
170
178
 
171
- class Queues
172
- def lengths
173
- Sidekiq.redis do |conn|
174
- queues = conn.sscan_each("queues").to_a
175
-
176
- lengths = conn.pipelined { |pipeline|
177
- queues.each do |queue|
178
- pipeline.llen("queue:#{queue}")
179
- end
180
- }
181
-
182
- array_of_arrays = queues.zip(lengths).sort_by { |_, size| -size }
183
- array_of_arrays.to_h
184
- end
185
- end
186
- end
187
-
188
179
  class History
189
- def initialize(days_previous, start_date = nil)
180
+ def initialize(days_previous, start_date = nil, pool: nil)
190
181
  # we only store five years of data in Redis
191
182
  raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
192
183
  @days_previous = days_previous
@@ -211,15 +202,10 @@ module Sidekiq
211
202
 
212
203
  keys = dates.map { |datestr| "stat:#{stat}:#{datestr}" }
213
204
 
214
- begin
215
- Sidekiq.redis do |conn|
216
- conn.mget(keys).each_with_index do |value, idx|
217
- stat_hash[dates[idx]] = value ? value.to_i : 0
218
- end
205
+ Sidekiq.redis do |conn|
206
+ conn.mget(keys).each_with_index do |value, idx|
207
+ stat_hash[dates[idx]] = value ? value.to_i : 0
219
208
  end
220
- rescue RedisConnection.adapter::CommandError
221
- # mget will trigger a CROSSSLOT error when run against a Cluster
222
- # TODO Someone want to add Cluster support?
223
209
  end
224
210
 
225
211
  stat_hash
@@ -247,7 +233,7 @@ module Sidekiq
247
233
  #
248
234
  # @return [Array<Sidekiq::Queue>]
249
235
  def self.all
250
- Sidekiq.redis { |c| c.sscan_each("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
236
+ Sidekiq.redis { |c| c.sscan("queues").to_a }.sort.map { |q| Sidekiq::Queue.new(q) }
251
237
  end
252
238
 
253
239
  attr_reader :name
@@ -278,8 +264,8 @@ module Sidekiq
278
264
  # @return [Float] in seconds
279
265
  def latency
280
266
  entry = Sidekiq.redis { |conn|
281
- conn.lrange(@rname, -1, -1)
282
- }.first
267
+ conn.lindex(@rname, -1)
268
+ }
283
269
  return 0 unless entry
284
270
  job = Sidekiq.load_json(entry)
285
271
  now = Time.now.to_f
@@ -388,12 +374,7 @@ module Sidekiq
388
374
  def display_class
389
375
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
390
376
  @klass ||= self["display_class"] || begin
391
- case klass
392
- when /\ASidekiq::Extensions::Delayed/
393
- safe_load(args[0], klass) do |target, method, _|
394
- "#{target}.#{method}"
395
- end
396
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
377
+ if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
397
378
  job_class = @item["wrapped"] || args[0]
398
379
  if job_class == "ActionMailer::DeliveryJob" || job_class == "ActionMailer::MailDeliveryJob"
399
380
  # MailerClass#mailer_method
@@ -409,23 +390,14 @@ module Sidekiq
409
390
 
410
391
  def display_args
411
392
  # Unwrap known wrappers so they show up in a human-friendly manner in the Web UI
412
- @display_args ||= case klass
413
- when /\ASidekiq::Extensions::Delayed/
414
- safe_load(args[0], args) do |_, _, arg, kwarg|
415
- if !kwarg || kwarg.empty?
416
- arg
417
- else
418
- [arg, kwarg]
419
- end
420
- end
421
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
422
- job_args = self["wrapped"] ? args[0]["arguments"] : []
393
+ @display_args ||= if klass == "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
394
+ job_args = self["wrapped"] ? deserialize_argument(args[0]["arguments"]) : []
423
395
  if (self["wrapped"] || args[0]) == "ActionMailer::DeliveryJob"
424
396
  # remove MailerClass, mailer_method and 'deliver_now'
425
397
  job_args.drop(3)
426
398
  elsif (self["wrapped"] || args[0]) == "ActionMailer::MailDeliveryJob"
427
399
  # remove MailerClass, mailer_method and 'deliver_now'
428
- job_args.drop(3).first["args"]
400
+ job_args.drop(3).first.values_at("params", "args")
429
401
  else
430
402
  job_args
431
403
  end
@@ -446,6 +418,10 @@ module Sidekiq
446
418
  self["jid"]
447
419
  end
448
420
 
421
+ def bid
422
+ self["bid"]
423
+ end
424
+
449
425
  def enqueued_at
450
426
  self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
451
427
  end
@@ -491,32 +467,34 @@ module Sidekiq
491
467
 
492
468
  private
493
469
 
494
- def safe_load(content, default)
495
- yield(*YAML.load(content))
496
- rescue => ex
497
- # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
498
- # memory yet so the YAML can't be loaded.
499
- # TODO is this still necessary? Zeitwerk reloader should handle?
500
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
501
- default
502
- end
470
+ ACTIVE_JOB_PREFIX = "_aj_"
471
+ GLOBALID_KEY = "_aj_globalid"
503
472
 
504
- def uncompress_backtrace(backtrace)
505
- if backtrace.is_a?(Array)
506
- # Handle old jobs with raw Array backtrace format
507
- backtrace
508
- else
509
- decoded = Base64.decode64(backtrace)
510
- uncompressed = Zlib::Inflate.inflate(decoded)
511
- begin
512
- Sidekiq.load_json(uncompressed)
513
- rescue
514
- # Handle old jobs with marshalled backtrace format
515
- # TODO Remove in 7.x
516
- Marshal.load(uncompressed)
473
+ def deserialize_argument(argument)
474
+ case argument
475
+ when Array
476
+ argument.map { |arg| deserialize_argument(arg) }
477
+ when Hash
478
+ if serialized_global_id?(argument)
479
+ argument[GLOBALID_KEY]
480
+ else
481
+ argument.transform_values { |v| deserialize_argument(v) }
482
+ .reject { |k, _| k.start_with?(ACTIVE_JOB_PREFIX) }
517
483
  end
484
+ else
485
+ argument
518
486
  end
519
487
  end
488
+
489
+ def serialized_global_id?(hash)
490
+ hash.size == 1 && hash.include?(GLOBALID_KEY)
491
+ end
492
+
493
+ def uncompress_backtrace(backtrace)
494
+ decoded = Base64.decode64(backtrace)
495
+ uncompressed = Zlib::Inflate.inflate(decoded)
496
+ Sidekiq.load_json(uncompressed)
497
+ end
520
498
  end
521
499
 
522
500
  # Represents a job within a Redis sorted set where the score
@@ -593,7 +571,7 @@ module Sidekiq
593
571
  def remove_job
594
572
  Sidekiq.redis do |conn|
595
573
  results = conn.multi { |transaction|
596
- transaction.zrangebyscore(parent.name, score, score)
574
+ transaction.zrange(parent.name, score, score, "BYSCORE")
597
575
  transaction.zremrangebyscore(parent.name, score, score)
598
576
  }.first
599
577
 
@@ -656,7 +634,7 @@ module Sidekiq
656
634
 
657
635
  match = "*#{match}*" unless match.include?("*")
658
636
  Sidekiq.redis do |conn|
659
- conn.zscan_each(name, match: match, count: count) do |entry, score|
637
+ conn.zscan(name, match: match, count: count) do |entry, score|
660
638
  yield SortedEntry.new(self, score, entry)
661
639
  end
662
640
  end
@@ -728,7 +706,7 @@ module Sidekiq
728
706
  end
729
707
 
730
708
  elements = Sidekiq.redis { |conn|
731
- conn.zrangebyscore(name, begin_score, end_score, withscores: true)
709
+ conn.zrange(name, begin_score, end_score, "BYSCORE", withscores: true)
732
710
  }
733
711
 
734
712
  elements.each_with_object([]) do |element, result|
@@ -746,8 +724,8 @@ module Sidekiq
746
724
  # @return [SortedEntry] the record or nil
747
725
  def find_job(jid)
748
726
  Sidekiq.redis do |conn|
749
- conn.zscan_each(name, match: "*#{jid}*", count: 100) do |entry, score|
750
- job = JSON.parse(entry)
727
+ conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
728
+ job = Sidekiq.load_json(entry)
751
729
  matched = job["jid"] == jid
752
730
  return SortedEntry.new(self, score, entry) if matched
753
731
  end
@@ -769,7 +747,7 @@ module Sidekiq
769
747
  # @api private
770
748
  def delete_by_jid(score, jid)
771
749
  Sidekiq.redis do |conn|
772
- elements = conn.zrangebyscore(name, score, score)
750
+ elements = conn.zrange(name, score, score, "BYSCORE")
773
751
  elements.each do |element|
774
752
  if element.index(jid)
775
753
  message = Sidekiq.load_json(element)
@@ -792,12 +770,8 @@ module Sidekiq
792
770
  # example where I'm selecting jobs based on some complex logic
793
771
  # and deleting them from the scheduled set.
794
772
  #
795
- # r = Sidekiq::ScheduledSet.new
796
- # r.select do |scheduled|
797
- # scheduled.klass == 'Sidekiq::Extensions::DelayedClass' &&
798
- # scheduled.args[0] == 'User' &&
799
- # scheduled.args[1] == 'setup_new_subscriber'
800
- # end.map(&:delete)
773
+ # See the API wiki page for usage notes and examples.
774
+ #
801
775
  class ScheduledSet < JobSet
802
776
  def initialize
803
777
  super "schedule"
@@ -810,12 +784,8 @@ module Sidekiq
810
784
  # example where I'm selecting all jobs of a certain type
811
785
  # and deleting them from the retry queue.
812
786
  #
813
- # r = Sidekiq::RetrySet.new
814
- # r.select do |retri|
815
- # retri.klass == 'Sidekiq::Extensions::DelayedClass' &&
816
- # retri.args[0] == 'User' &&
817
- # retri.args[1] == 'setup_new_subscriber'
818
- # end.map(&:delete)
787
+ # See the API wiki page for usage notes and examples.
788
+ #
819
789
  class RetrySet < JobSet
820
790
  def initialize
821
791
  super "retry"
@@ -849,8 +819,8 @@ module Sidekiq
849
819
  Sidekiq.redis do |conn|
850
820
  conn.multi do |transaction|
851
821
  transaction.zadd(name, now.to_s, message)
852
- transaction.zremrangebyscore(name, "-inf", now - self.class.timeout)
853
- transaction.zremrangebyrank(name, 0, - self.class.max_jobs)
822
+ transaction.zremrangebyscore(name, "-inf", now - Sidekiq::Config::DEFAULTS[:dead_timeout_in_seconds])
823
+ transaction.zremrangebyrank(name, 0, - Sidekiq::Config::DEFAULTS[:dead_max_jobs])
854
824
  end
855
825
  end
856
826
 
@@ -858,7 +828,7 @@ module Sidekiq
858
828
  job = Sidekiq.load_json(message)
859
829
  r = RuntimeError.new("Job killed by API")
860
830
  r.set_backtrace(caller)
861
- Sidekiq.death_handlers.each do |handle|
831
+ Sidekiq.default_configuration.death_handlers.each do |handle|
862
832
  handle.call(job, r)
863
833
  end
864
834
  end
@@ -869,18 +839,6 @@ module Sidekiq
869
839
  def retry_all
870
840
  each(&:retry) while size > 0
871
841
  end
872
-
873
- # The maximum size of the Dead set. Older entries will be trimmed
874
- # to stay within this limit. Default value is 10,000.
875
- def self.max_jobs
876
- Sidekiq[:dead_max_jobs]
877
- end
878
-
879
- # The time limit for entries within the Dead set. Older entries will be thrown away.
880
- # Default value is six months.
881
- def self.timeout
882
- Sidekiq[:dead_timeout_in_seconds]
883
- end
884
842
  end
885
843
 
886
844
  ##
@@ -893,6 +851,24 @@ module Sidekiq
893
851
  class ProcessSet
894
852
  include Enumerable
895
853
 
854
+ def self.[](identity)
855
+ exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
856
+ conn.multi { |transaction|
857
+ transaction.sismember("processes", identity)
858
+ transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
859
+ }
860
+ }
861
+
862
+ return nil if exists == 0 || info.nil?
863
+
864
+ hash = Sidekiq.load_json(info)
865
+ Process.new(hash.merge("busy" => busy.to_i,
866
+ "beat" => beat.to_f,
867
+ "quiet" => quiet,
868
+ "rss" => rss.to_i,
869
+ "rtt_us" => rtt_us.to_i))
870
+ end
871
+
896
872
  # :nodoc:
897
873
  # @api private
898
874
  def initialize(clean_plz = true)
@@ -909,7 +885,7 @@ module Sidekiq
909
885
 
910
886
  count = 0
911
887
  Sidekiq.redis do |conn|
912
- procs = conn.sscan_each("processes").to_a
888
+ procs = conn.sscan("processes").to_a
913
889
  heartbeats = conn.pipelined { |pipeline|
914
890
  procs.each do |key|
915
891
  pipeline.hget(key, "info")
@@ -929,7 +905,7 @@ module Sidekiq
929
905
 
930
906
  def each
931
907
  result = Sidekiq.redis { |conn|
932
- procs = conn.sscan_each("processes").to_a.sort
908
+ procs = conn.sscan("processes").to_a.sort
933
909
 
934
910
  # We're making a tradeoff here between consuming more memory instead of
935
911
  # making more roundtrips to Redis, but if you have hundreds or thousands of workers,
@@ -941,7 +917,7 @@ module Sidekiq
941
917
  end
942
918
  }
943
919
 
944
- result.each do |info, busy, at_s, quiet, rss, rtt|
920
+ result.each do |info, busy, beat, quiet, rss, rtt_us|
945
921
  # If a process is stopped between when we query Redis for `procs` and
946
922
  # when we query for `result`, we will have an item in `result` that is
947
923
  # composed of `nil` values.
@@ -949,10 +925,10 @@ module Sidekiq
949
925
 
950
926
  hash = Sidekiq.load_json(info)
951
927
  yield Process.new(hash.merge("busy" => busy.to_i,
952
- "beat" => at_s.to_f,
928
+ "beat" => beat.to_f,
953
929
  "quiet" => quiet,
954
930
  "rss" => rss.to_i,
955
- "rtt_us" => rtt.to_i))
931
+ "rtt_us" => rtt_us.to_i))
956
932
  end
957
933
  end
958
934
 
@@ -1008,6 +984,7 @@ module Sidekiq
1008
984
  # 'busy' => 10,
1009
985
  # 'beat' => <last heartbeat>,
1010
986
  # 'identity' => <unique string identifying the process>,
987
+ # 'embedded' => true,
1011
988
  # }
1012
989
  class Process
1013
990
  # :nodoc:
@@ -1021,7 +998,7 @@ module Sidekiq
1021
998
  end
1022
999
 
1023
1000
  def labels
1024
- Array(self["labels"])
1001
+ self["labels"].to_a
1025
1002
  end
1026
1003
 
1027
1004
  def [](key)
@@ -1036,11 +1013,25 @@ module Sidekiq
1036
1013
  self["queues"]
1037
1014
  end
1038
1015
 
1016
+ def weights
1017
+ self["weights"]
1018
+ end
1019
+
1020
+ def version
1021
+ self["version"]
1022
+ end
1023
+
1024
+ def embedded?
1025
+ self["embedded"]
1026
+ end
1027
+
1039
1028
  # Signal this process to stop processing new jobs.
1040
1029
  # It will continue to execute jobs it has already fetched.
1041
1030
  # This method is *asynchronous* and it can take 5-10
1042
1031
  # seconds for the process to quiet.
1043
1032
  def quiet!
1033
+ raise "Can't quiet an embedded process" if embedded?
1034
+
1044
1035
  signal("TSTP")
1045
1036
  end
1046
1037
 
@@ -1049,6 +1040,8 @@ module Sidekiq
1049
1040
  # This method is *asynchronous* and it can take 5-10
1050
1041
  # seconds for the process to start shutting down.
1051
1042
  def stop!
1043
+ raise "Can't stop an embedded process" if embedded?
1044
+
1052
1045
  signal("TERM")
1053
1046
  end
1054
1047
 
@@ -1107,8 +1100,7 @@ module Sidekiq
1107
1100
  all_works = nil
1108
1101
 
1109
1102
  Sidekiq.redis do |conn|
1110
- procs = conn.sscan_each("processes").to_a.sort
1111
-
1103
+ procs = conn.sscan("processes").to_a.sort
1112
1104
  all_works = conn.pipelined do |pipeline|
1113
1105
  procs.each do |key|
1114
1106
  pipeline.hgetall("#{key}:work")
@@ -1118,13 +1110,7 @@ module Sidekiq
1118
1110
 
1119
1111
  procs.zip(all_works).each do |key, workers|
1120
1112
  workers.each_pair do |tid, json|
1121
- next if json.empty?
1122
-
1123
- hsh = Sidekiq.load_json(json)
1124
- p = hsh["payload"]
1125
- # avoid breaking API, this is a side effect of the JSON optimization in #4316
1126
- hsh["payload"] = Sidekiq.load_json(p) if p.is_a?(String)
1127
- results << [key, tid, hsh]
1113
+ results << [key, tid, Sidekiq.load_json(json)] unless json.empty?
1128
1114
  end
1129
1115
  end
1130
1116
 
@@ -1139,7 +1125,7 @@ module Sidekiq
1139
1125
  # which can easily get out of sync with crashy processes.
1140
1126
  def size
1141
1127
  Sidekiq.redis do |conn|
1142
- procs = conn.sscan_each("processes").to_a
1128
+ procs = conn.sscan("processes").to_a
1143
1129
  if procs.empty?
1144
1130
  0
1145
1131
  else
@@ -0,0 +1,127 @@
1
+ require "sidekiq/component"
2
+
3
+ module Sidekiq
4
+ # A Sidekiq::Capsule is the set of resources necessary to
5
+ # process one or more queues with a given concurrency.
6
+ # One "default" Capsule is started but the user may declare additional
7
+ # Capsules in their initializer.
8
+ #
9
+ # This capsule will pull jobs from the "single" queue and process
10
+ # the jobs with one thread, meaning the jobs will be processed serially.
11
+ #
12
+ # Sidekiq.configure_server do |config|
13
+ # config.capsule("single-threaded") do |cap|
14
+ # cap.concurrency = 1
15
+ # cap.queues = %w(single)
16
+ # end
17
+ # end
18
+ class Capsule
19
+ include Sidekiq::Component
20
+
21
+ attr_reader :name
22
+ attr_reader :queues
23
+ attr_accessor :concurrency
24
+ attr_reader :mode
25
+ attr_reader :weights
26
+
27
+ def initialize(name, config)
28
+ @name = name
29
+ @config = config
30
+ @queues = ["default"]
31
+ @weights = {"default" => 0}
32
+ @concurrency = config[:concurrency]
33
+ @mode = :strict
34
+ end
35
+
36
+ def fetcher
37
+ @fetcher ||= begin
38
+ inst = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
39
+ inst.setup(config[:fetch_setup]) if inst.respond_to?(:setup)
40
+ inst
41
+ end
42
+ end
43
+
44
+ def stop
45
+ fetcher&.bulk_requeue([])
46
+ end
47
+
48
+ # Sidekiq checks queues in three modes:
49
+ # - :strict - all queues have 0 weight and are checked strictly in order
50
+ # - :weighted - queues have arbitrary weight between 1 and N
51
+ # - :random - all queues have weight of 1
52
+ def queues=(val)
53
+ @weights = {}
54
+ @queues = Array(val).each_with_object([]) do |qstr, memo|
55
+ arr = qstr
56
+ arr = qstr.split(",") if qstr.is_a?(String)
57
+ name, weight = arr
58
+ @weights[name] = weight.to_i
59
+ [weight.to_i, 1].max.times do
60
+ memo << name
61
+ end
62
+ end
63
+ @mode = if @weights.values.all?(&:zero?)
64
+ :strict
65
+ elsif @weights.values.all? { |x| x == 1 }
66
+ :random
67
+ else
68
+ :weighted
69
+ end
70
+ end
71
+
72
+ # Allow the middleware to be different per-capsule.
73
+ # Avoid if possible and add middleware globally so all
74
+ # capsules share the same chains. Easier to debug that way.
75
+ def client_middleware
76
+ @client_chain ||= config.client_middleware.copy_for(self)
77
+ yield @client_chain if block_given?
78
+ @client_chain
79
+ end
80
+
81
+ def server_middleware
82
+ @server_chain ||= config.server_middleware.copy_for(self)
83
+ yield @server_chain if block_given?
84
+ @server_chain
85
+ end
86
+
87
+ def redis_pool
88
+ Thread.current[:sidekiq_redis_pool] || local_redis_pool
89
+ end
90
+
91
+ def local_redis_pool
92
+ # connection pool is lazy, it will not create connections unless you actually need them
93
+ # so don't be skimpy!
94
+ @redis ||= config.new_redis_pool(@concurrency, name)
95
+ end
96
+
97
+ def redis
98
+ raise ArgumentError, "requires a block" unless block_given?
99
+ redis_pool.with do |conn|
100
+ retryable = true
101
+ begin
102
+ yield conn
103
+ rescue RedisClientAdapter::BaseError => ex
104
+ # 2550 Failover can cause the server to become a replica, need
105
+ # to disconnect and reopen the socket to get back to the primary.
106
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
107
+ # 4985 Use the same logic when a blocking command is force-unblocked
108
+ # The same retry logic is also used in client.rb
109
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
110
+ conn.close
111
+ retryable = false
112
+ retry
113
+ end
114
+ raise
115
+ end
116
+ end
117
+ end
118
+
119
+ def lookup(name)
120
+ config.lookup(name)
121
+ end
122
+
123
+ def logger
124
+ config.logger
125
+ end
126
+ end
127
+ end