sidekiq 7.0.1 → 7.0.8

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +69 -9
  3. data/README.md +29 -22
  4. data/bin/sidekiqload +186 -109
  5. data/bin/sidekiqmon +3 -0
  6. data/lib/sidekiq/api.rb +43 -4
  7. data/lib/sidekiq/capsule.rb +17 -0
  8. data/lib/sidekiq/cli.rb +3 -2
  9. data/lib/sidekiq/client.rb +1 -1
  10. data/lib/sidekiq/component.rb +2 -0
  11. data/lib/sidekiq/config.rb +2 -3
  12. data/lib/sidekiq/deploy.rb +3 -3
  13. data/lib/sidekiq/embedded.rb +1 -1
  14. data/lib/sidekiq/fetch.rb +3 -5
  15. data/lib/sidekiq/job.rb +2 -2
  16. data/lib/sidekiq/job_logger.rb +1 -1
  17. data/lib/sidekiq/job_retry.rb +5 -4
  18. data/lib/sidekiq/job_util.rb +48 -14
  19. data/lib/sidekiq/launcher.rb +13 -7
  20. data/lib/sidekiq/metrics/query.rb +1 -1
  21. data/lib/sidekiq/metrics/tracking.rb +2 -0
  22. data/lib/sidekiq/middleware/chain.rb +12 -9
  23. data/lib/sidekiq/middleware/current_attributes.rb +5 -7
  24. data/lib/sidekiq/monitor.rb +17 -4
  25. data/lib/sidekiq/paginator.rb +2 -2
  26. data/lib/sidekiq/processor.rb +4 -1
  27. data/lib/sidekiq/rails.rb +2 -1
  28. data/lib/sidekiq/redis_client_adapter.rb +3 -6
  29. data/lib/sidekiq/scheduled.rb +1 -1
  30. data/lib/sidekiq/version.rb +1 -1
  31. data/lib/sidekiq/web/application.rb +20 -5
  32. data/lib/sidekiq/web/helpers.rb +15 -7
  33. data/lib/sidekiq/web.rb +4 -0
  34. data/sidekiq.gemspec +11 -22
  35. data/web/assets/javascripts/application.js +18 -0
  36. data/web/assets/javascripts/metrics.js +30 -2
  37. data/web/assets/stylesheets/application-dark.css +4 -0
  38. data/web/assets/stylesheets/application.css +3 -3
  39. data/web/locales/da.yml +11 -4
  40. data/web/locales/ja.yml +3 -1
  41. data/web/views/_footer.erb +2 -2
  42. data/web/views/_job_info.erb +18 -2
  43. data/web/views/_metrics_period_select.erb +12 -0
  44. data/web/views/_paging.erb +2 -0
  45. data/web/views/busy.erb +37 -26
  46. data/web/views/metrics.erb +6 -4
  47. data/web/views/metrics_for_job.erb +9 -7
  48. data/web/views/morgue.erb +5 -9
  49. data/web/views/queue.erb +10 -14
  50. data/web/views/queues.erb +3 -1
  51. data/web/views/retries.erb +5 -9
  52. data/web/views/scheduled.erb +12 -13
  53. metadata +17 -26
data/lib/sidekiq/api.rb CHANGED
@@ -418,6 +418,10 @@ module Sidekiq
418
418
  self["jid"]
419
419
  end
420
420
 
421
+ def bid
422
+ self["bid"]
423
+ end
424
+
421
425
  def enqueued_at
422
426
  self["enqueued_at"] ? Time.at(self["enqueued_at"]).utc : nil
423
427
  end
@@ -698,7 +702,7 @@ module Sidekiq
698
702
  def find_job(jid)
699
703
  Sidekiq.redis do |conn|
700
704
  conn.zscan(name, match: "*#{jid}*", count: 100) do |entry, score|
701
- job = JSON.parse(entry)
705
+ job = Sidekiq.load_json(entry)
702
706
  matched = job["jid"] == jid
703
707
  return SortedEntry.new(self, score, entry) if matched
704
708
  end
@@ -824,6 +828,24 @@ module Sidekiq
824
828
  class ProcessSet
825
829
  include Enumerable
826
830
 
831
+ def self.[](identity)
832
+ exists, (info, busy, beat, quiet, rss, rtt_us) = Sidekiq.redis { |conn|
833
+ conn.multi { |transaction|
834
+ transaction.sismember("processes", identity)
835
+ transaction.hmget(identity, "info", "busy", "beat", "quiet", "rss", "rtt_us")
836
+ }
837
+ }
838
+
839
+ return nil if exists == 0 || info.nil?
840
+
841
+ hash = Sidekiq.load_json(info)
842
+ Process.new(hash.merge("busy" => busy.to_i,
843
+ "beat" => beat.to_f,
844
+ "quiet" => quiet,
845
+ "rss" => rss.to_i,
846
+ "rtt_us" => rtt_us.to_i))
847
+ end
848
+
827
849
  # :nodoc:
828
850
  # @api private
829
851
  def initialize(clean_plz = true)
@@ -872,7 +894,7 @@ module Sidekiq
872
894
  end
873
895
  }
874
896
 
875
- result.each do |info, busy, at_s, quiet, rss, rtt|
897
+ result.each do |info, busy, beat, quiet, rss, rtt_us|
876
898
  # If a process is stopped between when we query Redis for `procs` and
877
899
  # when we query for `result`, we will have an item in `result` that is
878
900
  # composed of `nil` values.
@@ -880,10 +902,10 @@ module Sidekiq
880
902
 
881
903
  hash = Sidekiq.load_json(info)
882
904
  yield Process.new(hash.merge("busy" => busy.to_i,
883
- "beat" => at_s.to_f,
905
+ "beat" => beat.to_f,
884
906
  "quiet" => quiet,
885
907
  "rss" => rss.to_i,
886
- "rtt_us" => rtt.to_i))
908
+ "rtt_us" => rtt_us.to_i))
887
909
  end
888
910
  end
889
911
 
@@ -939,6 +961,7 @@ module Sidekiq
939
961
  # 'busy' => 10,
940
962
  # 'beat' => <last heartbeat>,
941
963
  # 'identity' => <unique string identifying the process>,
964
+ # 'embedded' => true,
942
965
  # }
943
966
  class Process
944
967
  # :nodoc:
@@ -967,11 +990,25 @@ module Sidekiq
967
990
  self["queues"]
968
991
  end
969
992
 
993
+ def weights
994
+ self["weights"]
995
+ end
996
+
997
+ def version
998
+ self["version"]
999
+ end
1000
+
1001
+ def embedded?
1002
+ self["embedded"]
1003
+ end
1004
+
970
1005
  # Signal this process to stop processing new jobs.
971
1006
  # It will continue to execute jobs it has already fetched.
972
1007
  # This method is *asynchronous* and it can take 5-10
973
1008
  # seconds for the process to quiet.
974
1009
  def quiet!
1010
+ raise "Can't quiet an embedded process" if embedded?
1011
+
975
1012
  signal("TSTP")
976
1013
  end
977
1014
 
@@ -980,6 +1017,8 @@ module Sidekiq
980
1017
  # This method is *asynchronous* and it can take 5-10
981
1018
  # seconds for the process to start shutting down.
982
1019
  def stop!
1020
+ raise "Can't stop an embedded process" if embedded?
1021
+
983
1022
  signal("TERM")
984
1023
  end
985
1024
 
@@ -21,12 +21,16 @@ module Sidekiq
21
21
  attr_reader :name
22
22
  attr_reader :queues
23
23
  attr_accessor :concurrency
24
+ attr_reader :mode
25
+ attr_reader :weights
24
26
 
25
27
  def initialize(name, config)
26
28
  @name = name
27
29
  @config = config
28
30
  @queues = ["default"]
31
+ @weights = {"default" => 0}
29
32
  @concurrency = config[:concurrency]
33
+ @mode = :strict
30
34
  end
31
35
 
32
36
  def fetcher
@@ -41,15 +45,28 @@ module Sidekiq
41
45
  fetcher&.bulk_requeue([])
42
46
  end
43
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
44
52
  def queues=(val)
53
+ @weights = {}
45
54
  @queues = Array(val).each_with_object([]) do |qstr, memo|
46
55
  arr = qstr
47
56
  arr = qstr.split(",") if qstr.is_a?(String)
48
57
  name, weight = arr
58
+ @weights[name] = weight.to_i
49
59
  [weight.to_i, 1].max.times do
50
60
  memo << name
51
61
  end
52
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
53
70
  end
54
71
 
55
72
  # Allow the middleware to be different per-capsule.
data/lib/sidekiq/cli.rb CHANGED
@@ -77,13 +77,14 @@ module Sidekiq # :nodoc:
77
77
  raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
78
78
 
79
79
  maxmemory_policy = info["maxmemory_policy"]
80
- if maxmemory_policy != "noeviction"
80
+ if maxmemory_policy != "noeviction" && maxmemory_policy != ""
81
+ # Redis Enterprise Cloud returns "" for their policy 😳
81
82
  logger.warn <<~EOM
82
83
 
83
84
 
84
85
  WARNING: Your Redis instance will evict Sidekiq data under heavy load.
85
86
  The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
86
- See: https://github.com/mperham/sidekiq/wiki/Using-Redis#memory
87
+ See: https://github.com/sidekiq/sidekiq/wiki/Using-Redis#memory
87
88
 
88
89
  EOM
89
90
  end
@@ -189,7 +189,7 @@ module Sidekiq
189
189
  def enqueue_to_in(queue, interval, klass, *args)
190
190
  int = interval.to_f
191
191
  now = Time.now.to_f
192
- ts = (int < 1_000_000_000 ? now + int : int)
192
+ ts = ((int < 1_000_000_000) ? now + int : int)
193
193
 
194
194
  item = {"class" => klass, "args" => args, "at" => ts, "queue" => queue}
195
195
  item.delete("at") if ts <= now
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  ##
3
5
  # Sidekiq::Component assumes a config instance is available at @config
@@ -129,12 +129,12 @@ module Sidekiq
129
129
  def new_redis_pool(size, name = "unset")
130
130
  # connection pool is lazy, it will not create connections unless you actually need them
131
131
  # so don't be skimpy!
132
- RedisConnection.create(@redis_config.merge(size: size, logger: logger, pool_name: name))
132
+ RedisConnection.create({size: size, logger: logger, pool_name: name}.merge(@redis_config))
133
133
  end
134
134
 
135
135
  def redis_info
136
136
  redis do |conn|
137
- conn.info
137
+ conn.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
138
138
  rescue RedisClientAdapter::CommandError => ex
139
139
  # 2850 return fake version when INFO command has (probably) been renamed
140
140
  raise unless /unknown command/.match?(ex.message)
@@ -248,7 +248,6 @@ module Sidekiq
248
248
  return
249
249
  end
250
250
 
251
- logger.extend(Sidekiq::LoggingUtils)
252
251
  @logger = logger
253
252
  end
254
253
 
@@ -21,15 +21,15 @@ module Sidekiq
21
21
  }
22
22
 
23
23
  def self.mark!(label = nil)
24
- label ||= LABEL_MAKER.call
25
- Sidekiq::Deploy.new.mark(label: label)
24
+ Sidekiq::Deploy.new.mark!(label: label)
26
25
  end
27
26
 
28
27
  def initialize(pool = Sidekiq::RedisConnection.create)
29
28
  @pool = pool
30
29
  end
31
30
 
32
- def mark(at: Time.now, label: "")
31
+ def mark!(at: Time.now, label: nil)
32
+ label ||= LABEL_MAKER.call
33
33
  # we need to round the timestamp so that we gracefully
34
34
  # handle an very common error in marking deploys:
35
35
  # having every process mark its deploy, leading
@@ -49,7 +49,7 @@ module Sidekiq
49
49
 
50
50
  WARNING: Your Redis instance will evict Sidekiq data under heavy load.
51
51
  The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
52
- See: https://github.com/mperham/sidekiq/wiki/Using-Redis#memory
52
+ See: https://github.com/sidekiq/sidekiq/wiki/Using-Redis#memory
53
53
 
54
54
  EOM
55
55
  end
data/lib/sidekiq/fetch.rb CHANGED
@@ -30,11 +30,9 @@ module Sidekiq # :nodoc:
30
30
  def initialize(cap)
31
31
  raise ArgumentError, "missing queue list" unless cap.queues
32
32
  @config = cap
33
- @strictly_ordered_queues = (config.queues.size == config.queues.uniq.size)
33
+ @strictly_ordered_queues = cap.mode == :strict
34
34
  @queues = config.queues.map { |q| "queue:#{q}" }
35
- if @strictly_ordered_queues
36
- @queues.uniq!
37
- end
35
+ @queues.uniq! if @strictly_ordered_queues
38
36
  end
39
37
 
40
38
  def retrieve_work
@@ -46,7 +44,7 @@ module Sidekiq # :nodoc:
46
44
  return nil
47
45
  end
48
46
 
49
- queue, job = redis { |conn| conn.blocking_call(false, "brpop", *qs, TIMEOUT) }
47
+ queue, job = redis { |conn| conn.blocking_call(TIMEOUT + 1, "brpop", *qs, TIMEOUT) }
50
48
  UnitOfWork.new(queue, job, config) if queue
51
49
  end
52
50
 
data/lib/sidekiq/job.rb CHANGED
@@ -258,7 +258,7 @@ module Sidekiq
258
258
  def at(interval)
259
259
  int = interval.to_f
260
260
  now = Time.now.to_f
261
- ts = (int < 1_000_000_000 ? now + int : int)
261
+ ts = ((int < 1_000_000_000) ? now + int : int)
262
262
  # Optimization to enqueue something now that is scheduled to go out now or in the past
263
263
  @opts["at"] = ts if ts > now
264
264
  self
@@ -325,7 +325,7 @@ module Sidekiq
325
325
  def perform_in(interval, *args)
326
326
  int = interval.to_f
327
327
  now = Time.now.to_f
328
- ts = (int < 1_000_000_000 ? now + int : int)
328
+ ts = ((int < 1_000_000_000) ? now + int : int)
329
329
 
330
330
  item = {"class" => self, "args" => args}
331
331
 
@@ -33,7 +33,7 @@ module Sidekiq
33
33
 
34
34
  Thread.current[:sidekiq_context] = h
35
35
  level = job_hash["log_level"]
36
- if level
36
+ if level && @logger.respond_to?(:log_at)
37
37
  @logger.log_at(level, &block)
38
38
  else
39
39
  yield
@@ -49,7 +49,7 @@ module Sidekiq
49
49
  # The default number of retries is 25 which works out to about 3 weeks
50
50
  # You can change the default maximum number of retries in your initializer:
51
51
  #
52
- # Sidekiq.options[:max_retries] = 7
52
+ # Sidekiq.default_configuration[:max_retries] = 7
53
53
  #
54
54
  # or limit the number of retries for a particular job and send retries to
55
55
  # a low priority queue with:
@@ -171,7 +171,7 @@ module Sidekiq
171
171
  # Goodbye dear message, you (re)tried your best I'm sure.
172
172
  return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
173
173
 
174
- strategy, delay = delay_for(jobinst, count, exception)
174
+ strategy, delay = delay_for(jobinst, count, exception, msg)
175
175
  case strategy
176
176
  when :discard
177
177
  return # poof!
@@ -190,17 +190,18 @@ module Sidekiq
190
190
  end
191
191
 
192
192
  # returns (strategy, seconds)
193
- def delay_for(jobinst, count, exception)
193
+ def delay_for(jobinst, count, exception, msg)
194
194
  rv = begin
195
195
  # sidekiq_retry_in can return two different things:
196
196
  # 1. When to retry next, as an integer of seconds
197
197
  # 2. A symbol which re-routes the job elsewhere, e.g. :discard, :kill, :default
198
- jobinst&.sidekiq_retry_in_block&.call(count, exception)
198
+ jobinst&.sidekiq_retry_in_block&.call(count, exception, msg)
199
199
  rescue Exception => e
200
200
  handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
201
201
  nil
202
202
  end
203
203
 
204
+ rv = rv.to_i if rv.respond_to?(:to_i)
204
205
  delay = (count**4) + 15
205
206
  if Integer === rv && rv > 0
206
207
  delay = rv
@@ -17,18 +17,23 @@ module Sidekiq
17
17
 
18
18
  def verify_json(item)
19
19
  job_class = item["wrapped"] || item["class"]
20
- if Sidekiq::Config::DEFAULTS[:on_complex_arguments] == :raise
21
- msg = <<~EOM
22
- Job arguments to #{job_class} must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices.
23
- To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
24
- EOM
25
- raise(ArgumentError, msg) unless json_safe?(item)
26
- elsif Sidekiq::Config::DEFAULTS[:on_complex_arguments] == :warn
27
- warn <<~EOM unless json_safe?(item)
28
- Job arguments to #{job_class} do not serialize to JSON safely. This will raise an error in
29
- Sidekiq 7.0. See https://github.com/mperham/sidekiq/wiki/Best-Practices or raise an error today
30
- by calling `Sidekiq.strict_args!` during Sidekiq initialization.
31
- EOM
20
+ args = item["args"]
21
+ mode = Sidekiq::Config::DEFAULTS[:on_complex_arguments]
22
+
23
+ if mode == :raise || mode == :warn
24
+ if (unsafe_item = json_unsafe?(args))
25
+ msg = <<~EOM
26
+ Job arguments to #{job_class} must be native JSON types, but #{unsafe_item.inspect} is a #{unsafe_item.class}.
27
+ See https://github.com/sidekiq/sidekiq/wiki/Best-Practices.
28
+ To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
29
+ EOM
30
+
31
+ if mode == :raise
32
+ raise(ArgumentError, msg)
33
+ else
34
+ warn(msg)
35
+ end
36
+ end
32
37
  end
33
38
  end
34
39
 
@@ -64,8 +69,37 @@ module Sidekiq
64
69
 
65
70
  private
66
71
 
67
- def json_safe?(item)
68
- JSON.parse(JSON.dump(item["args"])) == item["args"]
72
+ RECURSIVE_JSON_UNSAFE = {
73
+ Integer => ->(val) {},
74
+ Float => ->(val) {},
75
+ TrueClass => ->(val) {},
76
+ FalseClass => ->(val) {},
77
+ NilClass => ->(val) {},
78
+ String => ->(val) {},
79
+ Array => ->(val) {
80
+ val.each do |e|
81
+ unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
82
+ return unsafe_item unless unsafe_item.nil?
83
+ end
84
+ nil
85
+ },
86
+ Hash => ->(val) {
87
+ val.each do |k, v|
88
+ return k unless String === k
89
+
90
+ unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
91
+ return unsafe_item unless unsafe_item.nil?
92
+ end
93
+ nil
94
+ }
95
+ }
96
+
97
+ RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
98
+ RECURSIVE_JSON_UNSAFE.compare_by_identity
99
+ private_constant :RECURSIVE_JSON_UNSAFE
100
+
101
+ def json_unsafe?(item)
102
+ RECURSIVE_JSON_UNSAFE[item.class].call(item)
69
103
  end
70
104
  end
71
105
  end
@@ -37,6 +37,7 @@ module Sidekiq
37
37
  # and instead have thread call Launcher#heartbeat every N seconds.
38
38
  def run(async_beat: true)
39
39
  Sidekiq.freeze!
40
+ logger.debug { @config.merge!({}) }
40
41
  @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
41
42
  @poller.start
42
43
  @managers.each(&:start)
@@ -161,7 +162,7 @@ module Sidekiq
161
162
  fails = procd = 0
162
163
  kb = memory_usage(::Process.pid)
163
164
 
164
- _, exists, _, _, msg = redis { |conn|
165
+ _, exists, _, _, signal = redis { |conn|
165
166
  conn.multi { |transaction|
166
167
  transaction.sadd("processes", [key])
167
168
  transaction.exists(key)
@@ -180,9 +181,7 @@ module Sidekiq
180
181
  fire_event(:heartbeat) unless exists > 0
181
182
  fire_event(:beat, oneshot: false)
182
183
 
183
- return unless msg
184
-
185
- ::Process.kill(msg, ::Process.pid)
184
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
186
185
  rescue => e
187
186
  # ignore all redis/network issues
188
187
  logger.error("heartbeat: #{e}")
@@ -216,7 +215,7 @@ module Sidekiq
216
215
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
217
216
  Ensure Redis is running in the same AZ or datacenter as Sidekiq.
218
217
  If these values are close to 100,000, that means your Sidekiq process may be
219
- CPU-saturated; reduce your concurrency and/or see https://github.com/mperham/sidekiq/discussions/5039
218
+ CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
220
219
  EOM
221
220
  RTT_READINGS.reset
222
221
  end
@@ -250,12 +249,19 @@ module Sidekiq
250
249
  "pid" => ::Process.pid,
251
250
  "tag" => @config[:tag] || "",
252
251
  "concurrency" => @config.total_concurrency,
253
- "queues" => @config.capsules.values.map { |cap| cap.queues }.flatten.uniq,
252
+ "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
253
+ "weights" => to_weights,
254
254
  "labels" => @config[:labels].to_a,
255
- "identity" => identity
255
+ "identity" => identity,
256
+ "version" => Sidekiq::VERSION,
257
+ "embedded" => @embedded
256
258
  }
257
259
  end
258
260
 
261
+ def to_weights
262
+ @config.capsules.values.map(&:weights)
263
+ end
264
+
259
265
  def to_json
260
266
  # this data changes infrequently so dump it to a string
261
267
  # now so we don't need to dump it every heartbeat.
@@ -123,7 +123,7 @@ module Sidekiq
123
123
  def series_avg(metric = "ms")
124
124
  series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
125
125
  completed = series.dig("p", bucket) - series.dig("f", bucket)
126
- result[bucket] = completed == 0 ? 0 : value.to_f / completed
126
+ result[bucket] = (completed == 0) ? 0 : value.to_f / completed
127
127
  end
128
128
  end
129
129
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "time"
2
4
  require "sidekiq"
3
5
  require "sidekiq/metrics/shared"
@@ -166,23 +166,26 @@ module Sidekiq
166
166
 
167
167
  # Used by Sidekiq to execute the middleware at runtime
168
168
  # @api private
169
- def invoke(*args)
169
+ def invoke(*args, &block)
170
170
  return yield if empty?
171
171
 
172
172
  chain = retrieve
173
- traverse_chain = proc do
174
- if chain.empty?
175
- yield
176
- else
177
- chain.shift.call(*args, &traverse_chain)
173
+ traverse(chain, 0, args, &block)
174
+ end
175
+
176
+ private
177
+
178
+ def traverse(chain, index, args, &block)
179
+ if index >= chain.size
180
+ yield
181
+ else
182
+ chain[index].call(*args) do
183
+ traverse(chain, index + 1, args, &block)
178
184
  end
179
185
  end
180
- traverse_chain.call
181
186
  end
182
187
  end
183
188
 
184
- private
185
-
186
189
  # Represents each link in the middleware chain
187
190
  # @api private
188
191
  class Entry
@@ -22,13 +22,11 @@ module Sidekiq
22
22
  end
23
23
 
24
24
  def call(_, job, _, _)
25
- attrs = @strklass.constantize.attributes
26
- if attrs.any?
27
- if job.has_key?("cattr")
28
- job["cattr"].merge!(attrs)
29
- else
30
- job["cattr"] = attrs
31
- end
25
+ if !job.has_key?("cattr")
26
+ attrs = @strklass.constantize.attributes
27
+ # Retries can push the job N times, we don't
28
+ # want retries to reset cattr. #5692, #5090
29
+ job["cattr"] = attrs if attrs.any?
32
30
  end
33
31
  yield
34
32
  end
@@ -16,8 +16,6 @@ class Sidekiq::Monitor
16
16
  return
17
17
  end
18
18
  send(section)
19
- rescue => e
20
- abort "Couldn't get status: #{e}"
21
19
  end
22
20
 
23
21
  def all
@@ -49,10 +47,25 @@ class Sidekiq::Monitor
49
47
  def processes
50
48
  puts "---- Processes (#{process_set.size}) ----"
51
49
  process_set.each_with_index do |process, index|
50
+ # Keep compatibility with legacy versions since we don't want to break sidekiqmon during rolling upgrades or downgrades.
51
+ #
52
+ # Before:
53
+ # ["default", "critical"]
54
+ #
55
+ # After:
56
+ # {"default" => 1, "critical" => 10}
57
+ queues =
58
+ if process["weights"]
59
+ process["weights"].sort_by { |queue| queue[0] }.map { |capsule| capsule.map { |name, weight| (weight > 0) ? "#{name}: #{weight}" : name }.join(", ") }
60
+ else
61
+ process["queues"].sort
62
+ end
63
+
52
64
  puts "#{process["identity"]} #{tags_for(process)}"
53
65
  puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
54
66
  puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
55
- puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
67
+ puts " Queues: #{split_multiline(queues, pad: 11)}"
68
+ puts " Version: #{process["version"] || "Unknown"}" if process["version"] != Sidekiq::VERSION
56
69
  puts "" unless (index + 1) == process_set.size
57
70
  end
58
71
  end
@@ -101,7 +114,7 @@ class Sidekiq::Monitor
101
114
  tags = [
102
115
  process["tag"],
103
116
  process["labels"],
104
- (process["quiet"] == "true" ? "quiet" : nil)
117
+ ((process["quiet"] == "true") ? "quiet" : nil)
105
118
  ].flatten.compact
106
119
  tags.any? ? "[#{tags.join("] [")}]" : nil
107
120
  end
@@ -3,7 +3,7 @@
3
3
  module Sidekiq
4
4
  module Paginator
5
5
  def page(key, pageidx = 1, page_size = 25, opts = nil)
6
- current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
6
+ current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
7
7
  pageidx = current_page - 1
8
8
  total_size = 0
9
9
  items = []
@@ -45,7 +45,7 @@ module Sidekiq
45
45
  end
46
46
 
47
47
  def page_items(items, pageidx = 1, page_size = 25)
48
- current_page = pageidx.to_i < 1 ? 1 : pageidx.to_i
48
+ current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
49
49
  pageidx = current_page - 1
50
50
  starting = pageidx * page_size
51
51
  items = items.to_a
@@ -146,6 +146,9 @@ module Sidekiq
146
146
  end
147
147
  end
148
148
 
149
+ IGNORE_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :never}
150
+ private_constant :IGNORE_SHUTDOWN_INTERRUPTS
151
+
149
152
  def process(uow)
150
153
  jobstr = uow.job
151
154
  queue = uow.queue_name
@@ -195,7 +198,7 @@ module Sidekiq
195
198
  ensure
196
199
  if ack
197
200
  # We don't want a shutdown signal to interrupt job acknowledgment.
198
- Thread.handle_interrupt(Sidekiq::Shutdown => :never) do
201
+ Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
199
202
  uow.acknowledge
200
203
  end
201
204
  end
data/lib/sidekiq/rails.rb CHANGED
@@ -11,7 +11,8 @@ module Sidekiq
11
11
  end
12
12
 
13
13
  def call
14
- @app.reloader.wrap do
14
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
15
+ @app.reloader.wrap(**params) do
15
16
  yield
16
17
  end
17
18
  end
@@ -9,10 +9,12 @@ module Sidekiq
9
9
  CommandError = RedisClient::CommandError
10
10
 
11
11
  module CompatMethods
12
+ # TODO Deprecate and remove this
12
13
  def info
13
14
  @client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
14
15
  end
15
16
 
17
+ # TODO Deprecate and remove this
16
18
  def evalsha(sha, keys, argv)
17
19
  @client.call("EVALSHA", sha, keys.size, *keys, *argv)
18
20
  end
@@ -34,12 +36,7 @@ module Sidekiq
34
36
  CompatClient = RedisClient::Decorator.create(CompatMethods)
35
37
 
36
38
  class CompatClient
37
- # underscore methods are not official API
38
- def _client
39
- @client
40
- end
41
-
42
- def _config
39
+ def config
43
40
  @client.config
44
41
  end
45
42