sidekiq 6.5.10 → 7.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +159 -18
  3. data/README.md +42 -34
  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 +56 -74
  10. data/lib/sidekiq/client.rb +66 -37
  11. data/lib/sidekiq/component.rb +4 -1
  12. data/lib/sidekiq/config.rb +287 -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 +33 -15
  19. data/lib/sidekiq/job_util.rb +51 -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 +3 -3
  24. data/lib/sidekiq/metrics/shared.rb +8 -7
  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 +16 -3
  29. data/lib/sidekiq/paginator.rb +1 -1
  30. data/lib/sidekiq/processor.rb +46 -51
  31. data/lib/sidekiq/rails.rb +7 -17
  32. data/lib/sidekiq/redis_client_adapter.rb +10 -69
  33. data/lib/sidekiq/redis_connection.rb +12 -111
  34. data/lib/sidekiq/scheduled.rb +21 -22
  35. data/lib/sidekiq/testing.rb +24 -39
  36. data/lib/sidekiq/transaction_aware_client.rb +4 -5
  37. data/lib/sidekiq/version.rb +2 -1
  38. data/lib/sidekiq/web/action.rb +3 -3
  39. data/lib/sidekiq/web/application.rb +76 -11
  40. data/lib/sidekiq/web/csrf_protection.rb +2 -2
  41. data/lib/sidekiq/web/helpers.rb +39 -24
  42. data/lib/sidekiq/web.rb +17 -16
  43. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  44. data/lib/sidekiq.rb +76 -274
  45. data/sidekiq.gemspec +12 -10
  46. data/web/assets/javascripts/application.js +18 -0
  47. data/web/assets/javascripts/base-charts.js +106 -0
  48. data/web/assets/javascripts/dashboard-charts.js +168 -0
  49. data/web/assets/javascripts/dashboard.js +3 -223
  50. data/web/assets/javascripts/metrics.js +117 -115
  51. data/web/assets/stylesheets/application-dark.css +4 -0
  52. data/web/assets/stylesheets/application-rtl.css +2 -91
  53. data/web/assets/stylesheets/application.css +23 -298
  54. data/web/locales/ar.yml +70 -70
  55. data/web/locales/cs.yml +62 -62
  56. data/web/locales/da.yml +60 -53
  57. data/web/locales/de.yml +65 -65
  58. data/web/locales/el.yml +2 -7
  59. data/web/locales/en.yml +78 -70
  60. data/web/locales/es.yml +68 -68
  61. data/web/locales/fa.yml +65 -65
  62. data/web/locales/fr.yml +81 -67
  63. data/web/locales/gd.yml +99 -0
  64. data/web/locales/he.yml +65 -64
  65. data/web/locales/hi.yml +59 -59
  66. data/web/locales/it.yml +53 -53
  67. data/web/locales/ja.yml +67 -69
  68. data/web/locales/ko.yml +52 -52
  69. data/web/locales/lt.yml +66 -66
  70. data/web/locales/nb.yml +61 -61
  71. data/web/locales/nl.yml +52 -52
  72. data/web/locales/pl.yml +45 -45
  73. data/web/locales/pt-br.yml +79 -69
  74. data/web/locales/pt.yml +51 -51
  75. data/web/locales/ru.yml +67 -66
  76. data/web/locales/sv.yml +53 -53
  77. data/web/locales/ta.yml +60 -60
  78. data/web/locales/uk.yml +62 -61
  79. data/web/locales/ur.yml +64 -64
  80. data/web/locales/vi.yml +67 -67
  81. data/web/locales/zh-cn.yml +20 -18
  82. data/web/locales/zh-tw.yml +10 -1
  83. data/web/views/_footer.erb +5 -2
  84. data/web/views/_job_info.erb +18 -2
  85. data/web/views/_metrics_period_select.erb +12 -0
  86. data/web/views/_paging.erb +2 -0
  87. data/web/views/_poll_link.erb +1 -1
  88. data/web/views/busy.erb +39 -28
  89. data/web/views/dashboard.erb +36 -5
  90. data/web/views/filtering.erb +7 -0
  91. data/web/views/metrics.erb +33 -20
  92. data/web/views/metrics_for_job.erb +25 -44
  93. data/web/views/morgue.erb +5 -9
  94. data/web/views/queue.erb +10 -14
  95. data/web/views/queues.erb +3 -1
  96. data/web/views/retries.erb +5 -9
  97. data/web/views/scheduled.erb +12 -13
  98. metadata +44 -39
  99. data/lib/sidekiq/delay.rb +0 -43
  100. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  101. data/lib/sidekiq/extensions/active_record.rb +0 -43
  102. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  103. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  104. data/lib/sidekiq/metrics/deploy.rb +0 -47
  105. data/lib/sidekiq/worker.rb +0 -370
  106. data/web/assets/javascripts/graph.js +0 -16
  107. /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