sidekiq 7.0.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +261 -13
  3. data/README.md +34 -27
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +204 -109
  6. data/bin/sidekiqmon +3 -0
  7. data/lib/sidekiq/api.rb +151 -23
  8. data/lib/sidekiq/capsule.rb +20 -0
  9. data/lib/sidekiq/cli.rb +9 -4
  10. data/lib/sidekiq/client.rb +40 -24
  11. data/lib/sidekiq/component.rb +3 -1
  12. data/lib/sidekiq/config.rb +32 -12
  13. data/lib/sidekiq/deploy.rb +5 -5
  14. data/lib/sidekiq/embedded.rb +3 -3
  15. data/lib/sidekiq/fetch.rb +3 -5
  16. data/lib/sidekiq/iterable_job.rb +53 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +22 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  19. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  20. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  21. data/lib/sidekiq/job/iterable.rb +231 -0
  22. data/lib/sidekiq/job.rb +17 -10
  23. data/lib/sidekiq/job_logger.rb +24 -11
  24. data/lib/sidekiq/job_retry.rb +34 -11
  25. data/lib/sidekiq/job_util.rb +51 -15
  26. data/lib/sidekiq/launcher.rb +38 -22
  27. data/lib/sidekiq/logger.rb +1 -1
  28. data/lib/sidekiq/metrics/query.rb +6 -3
  29. data/lib/sidekiq/metrics/shared.rb +4 -4
  30. data/lib/sidekiq/metrics/tracking.rb +9 -3
  31. data/lib/sidekiq/middleware/chain.rb +12 -9
  32. data/lib/sidekiq/middleware/current_attributes.rb +70 -17
  33. data/lib/sidekiq/monitor.rb +17 -4
  34. data/lib/sidekiq/paginator.rb +4 -4
  35. data/lib/sidekiq/processor.rb +41 -27
  36. data/lib/sidekiq/rails.rb +18 -8
  37. data/lib/sidekiq/redis_client_adapter.rb +31 -35
  38. data/lib/sidekiq/redis_connection.rb +29 -7
  39. data/lib/sidekiq/scheduled.rb +4 -4
  40. data/lib/sidekiq/testing.rb +27 -8
  41. data/lib/sidekiq/transaction_aware_client.rb +7 -0
  42. data/lib/sidekiq/version.rb +1 -1
  43. data/lib/sidekiq/web/action.rb +10 -4
  44. data/lib/sidekiq/web/application.rb +113 -16
  45. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  46. data/lib/sidekiq/web/helpers.rb +104 -33
  47. data/lib/sidekiq/web.rb +63 -2
  48. data/lib/sidekiq.rb +2 -1
  49. data/sidekiq.gemspec +8 -29
  50. data/web/assets/javascripts/application.js +45 -0
  51. data/web/assets/javascripts/dashboard-charts.js +38 -12
  52. data/web/assets/javascripts/dashboard.js +8 -10
  53. data/web/assets/javascripts/metrics.js +64 -2
  54. data/web/assets/stylesheets/application-dark.css +4 -0
  55. data/web/assets/stylesheets/application-rtl.css +10 -0
  56. data/web/assets/stylesheets/application.css +38 -4
  57. data/web/locales/da.yml +11 -4
  58. data/web/locales/en.yml +2 -0
  59. data/web/locales/fr.yml +14 -0
  60. data/web/locales/gd.yml +99 -0
  61. data/web/locales/ja.yml +3 -1
  62. data/web/locales/pt-br.yml +20 -0
  63. data/web/locales/tr.yml +101 -0
  64. data/web/locales/zh-cn.yml +20 -19
  65. data/web/views/_footer.erb +14 -2
  66. data/web/views/_job_info.erb +18 -2
  67. data/web/views/_metrics_period_select.erb +12 -0
  68. data/web/views/_paging.erb +2 -0
  69. data/web/views/_poll_link.erb +1 -1
  70. data/web/views/_summary.erb +7 -7
  71. data/web/views/busy.erb +46 -35
  72. data/web/views/dashboard.erb +25 -35
  73. data/web/views/filtering.erb +7 -0
  74. data/web/views/layout.erb +6 -6
  75. data/web/views/metrics.erb +42 -31
  76. data/web/views/metrics_for_job.erb +41 -51
  77. data/web/views/morgue.erb +5 -9
  78. data/web/views/queue.erb +10 -14
  79. data/web/views/queues.erb +9 -3
  80. data/web/views/retries.erb +5 -9
  81. data/web/views/scheduled.erb +12 -13
  82. metadata +37 -32
@@ -32,15 +32,18 @@ module Sidekiq
32
32
  @done = false
33
33
  end
34
34
 
35
- def run
35
+ # Start this Sidekiq instance. If an embedding process already
36
+ # has a heartbeat thread, caller can use `async_beat: false`
37
+ # and instead have thread call Launcher#heartbeat every N seconds.
38
+ def run(async_beat: true)
36
39
  Sidekiq.freeze!
37
- @thread = safe_thread("heartbeat", &method(:start_heartbeat))
40
+ logger.debug { @config.merge!({}) }
41
+ @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
38
42
  @poller.start
39
43
  @managers.each(&:start)
40
44
  end
41
45
 
42
46
  # Stops this instance from processing any more jobs,
43
- #
44
47
  def quiet
45
48
  return if @done
46
49
 
@@ -71,18 +74,30 @@ module Sidekiq
71
74
  @done
72
75
  end
73
76
 
77
+ # If embedding Sidekiq, you can have the process heartbeat
78
+ # call this method to regularly heartbeat rather than creating
79
+ # a separate thread.
80
+ def heartbeat
81
+
82
+ end
83
+
74
84
  private unless $TESTING
75
85
 
76
86
  BEAT_PAUSE = 10
77
87
 
78
88
  def start_heartbeat
79
89
  loop do
80
- heartbeat
90
+ beat
81
91
  sleep BEAT_PAUSE
82
92
  end
83
93
  logger.info("Heartbeat stopping...")
84
94
  end
85
95
 
96
+ def beat
97
+ $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
98
+
99
+ end
100
+
86
101
  def clear_heartbeat
87
102
  flush_stats
88
103
 
@@ -99,12 +114,6 @@ module Sidekiq
99
114
  # best effort, ignore network errors
100
115
  end
101
116
 
102
- def heartbeat
103
- $0 = PROCTITLES.map { |proc| proc.call(self, to_data) }.compact.join(" ") unless @embedded
104
-
105
-
106
- end
107
-
108
117
  def flush_stats
109
118
  fails = Processor::FAILURE.reset
110
119
  procd = Processor::PROCESSED.reset
@@ -136,15 +145,17 @@ module Sidekiq
136
145
  flush_stats
137
146
 
138
147
  curstate = Processor::WORK_STATE.dup
148
+ curstate.transform_values! { |val| Sidekiq.dump_json(val) }
149
+
139
150
  redis do |conn|
140
151
  # work is the current set of executing jobs
141
152
  work_key = "#{key}:work"
142
- conn.pipelined do |transaction|
153
+ conn.multi do |transaction|
143
154
  transaction.unlink(work_key)
144
- curstate.each_pair do |tid, hash|
145
- transaction.hset(work_key, tid, Sidekiq.dump_json(hash))
155
+ if curstate.size > 0
156
+ transaction.hset(work_key, curstate)
157
+ transaction.expire(work_key, 60)
146
158
  end
147
- transaction.expire(work_key, 60)
148
159
  end
149
160
  end
150
161
 
@@ -153,11 +164,11 @@ module Sidekiq
153
164
  fails = procd = 0
154
165
  kb = memory_usage(::Process.pid)
155
166
 
156
- _, exists, _, _, msg = redis { |conn|
167
+ _, exists, _, _, signal = redis { |conn|
157
168
  conn.multi { |transaction|
158
169
  transaction.sadd("processes", [key])
159
170
  transaction.exists(key)
160
- transaction.hmset(key, "info", to_json,
171
+ transaction.hset(key, "info", to_json,
161
172
  "busy", curstate.size,
162
173
  "beat", Time.now.to_f,
163
174
  "rtt_us", rtt,
@@ -172,9 +183,7 @@ module Sidekiq
172
183
  fire_event(:heartbeat) unless exists > 0
173
184
  fire_event(:beat, oneshot: false)
174
185
 
175
- return unless msg
176
-
177
- ::Process.kill(msg, ::Process.pid)
186
+ ::Process.kill(signal, ::Process.pid) if signal && !@embedded
178
187
  rescue => e
179
188
  # ignore all redis/network issues
180
189
  logger.error("heartbeat: #{e}")
@@ -208,7 +217,7 @@ module Sidekiq
208
217
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
209
218
  Ensure Redis is running in the same AZ or datacenter as Sidekiq.
210
219
  If these values are close to 100,000, that means your Sidekiq process may be
211
- CPU-saturated; reduce your concurrency and/or see https://github.com/mperham/sidekiq/discussions/5039
220
+ CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
212
221
  EOM
213
222
  RTT_READINGS.reset
214
223
  end
@@ -242,12 +251,19 @@ module Sidekiq
242
251
  "pid" => ::Process.pid,
243
252
  "tag" => @config[:tag] || "",
244
253
  "concurrency" => @config.total_concurrency,
245
- "queues" => @config.capsules.values.map { |cap| cap.queues }.flatten.uniq,
254
+ "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
255
+ "weights" => to_weights,
246
256
  "labels" => @config[:labels].to_a,
247
- "identity" => identity
257
+ "identity" => identity,
258
+ "version" => Sidekiq::VERSION,
259
+ "embedded" => @embedded
248
260
  }
249
261
  end
250
262
 
263
+ def to_weights
264
+ @config.capsules.values.map(&:weights)
265
+ end
266
+
251
267
  def to_json
252
268
  # this data changes infrequently so dump it to a string
253
269
  # now so we don't need to dump it every heartbeat.
@@ -36,7 +36,7 @@ module Sidekiq
36
36
  end
37
37
 
38
38
  LEVELS.each do |level, numeric_level|
39
- define_method("#{level}?") do
39
+ define_method(:"#{level}?") do
40
40
  local_level.nil? ? super() : local_level <= numeric_level
41
41
  end
42
42
  end
@@ -20,7 +20,8 @@ module Sidekiq
20
20
  end
21
21
 
22
22
  # Get metric data for all jobs from the last hour
23
- def top_jobs(minutes: 60)
23
+ # +class_filter+: return only results for classes matching filter
24
+ def top_jobs(class_filter: nil, minutes: 60)
24
25
  result = Result.new
25
26
 
26
27
  time = @time
@@ -39,6 +40,7 @@ module Sidekiq
39
40
  redis_results.each do |hash|
40
41
  hash.each do |k, v|
41
42
  kls, metric = k.split("|")
43
+ next if class_filter && !class_filter.match?(kls)
42
44
  result.job_results[kls].add_metric metric, time, v.to_i
43
45
  end
44
46
  time -= 60
@@ -70,7 +72,7 @@ module Sidekiq
70
72
  result.job_results[klass].add_metric "ms", time, ms.to_i if ms
71
73
  result.job_results[klass].add_metric "p", time, p.to_i if p
72
74
  result.job_results[klass].add_metric "f", time, f.to_i if f
73
- result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time)
75
+ result.job_results[klass].add_hist time, Histogram.new(klass).fetch(conn, time).reverse
74
76
  time -= 60
75
77
  end
76
78
  end
@@ -117,13 +119,14 @@ module Sidekiq
117
119
 
118
120
  def total_avg(metric = "ms")
119
121
  completed = totals["p"] - totals["f"]
122
+ return 0 if completed.zero?
120
123
  totals[metric].to_f / completed
121
124
  end
122
125
 
123
126
  def series_avg(metric = "ms")
124
127
  series[metric].each_with_object(Hash.new(0)) do |(bucket, value), result|
125
128
  completed = series.dig("p", bucket) - series.dig("f", bucket)
126
- result[bucket] = completed == 0 ? 0 : value.to_f / completed
129
+ result[bucket] = (completed == 0) ? 0 : value.to_f / completed
127
130
  end
128
131
  end
129
132
  end
@@ -29,8 +29,8 @@ module Sidekiq
29
29
  1100, 1700, 2500, 3800, 5750,
30
30
  8500, 13000, 20000, 30000, 45000,
31
31
  65000, 100000, 150000, 225000, 335000,
32
- Float::INFINITY # the "maybe your job is too long" bucket
33
- ]
32
+ 1e20 # the "maybe your job is too long" bucket
33
+ ].freeze
34
34
  LABELS = [
35
35
  "20ms", "30ms", "45ms", "65ms", "100ms",
36
36
  "150ms", "225ms", "335ms", "500ms", "750ms",
@@ -38,7 +38,7 @@ module Sidekiq
38
38
  "8.5s", "13s", "20s", "30s", "45s",
39
39
  "65s", "100s", "150s", "225s", "335s",
40
40
  "Slow"
41
- ]
41
+ ].freeze
42
42
  FETCH = "GET u16 #0 GET u16 #1 GET u16 #2 GET u16 #3 \
43
43
  GET u16 #4 GET u16 #5 GET u16 #6 GET u16 #7 \
44
44
  GET u16 #8 GET u16 #9 GET u16 #10 GET u16 #11 \
@@ -73,7 +73,7 @@ module Sidekiq
73
73
  def fetch(conn, now = Time.now)
74
74
  window = now.utc.strftime("%d-%H:%-M")
75
75
  key = "#{@klass}-#{window}"
76
- conn.bitfield(key, *FETCH)
76
+ conn.bitfield_ro(key, *FETCH)
77
77
  end
78
78
 
79
79
  def persist(conn, now = Time.now)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "time"
2
4
  require "sidekiq"
3
5
  require "sidekiq/metrics/shared"
@@ -101,12 +103,16 @@ module Sidekiq
101
103
  def reset
102
104
  @lock.synchronize {
103
105
  array = [@totals, @jobs, @grams]
104
- @totals = Hash.new(0)
105
- @jobs = Hash.new(0)
106
- @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
106
+ reset_instance_variables
107
107
  array
108
108
  }
109
109
  end
110
+
111
+ def reset_instance_variables
112
+ @totals = Hash.new(0)
113
+ @jobs = Hash.new(0)
114
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
115
+ end
110
116
  end
111
117
 
112
118
  class Middleware
@@ -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
@@ -7,27 +7,31 @@ module Sidekiq
7
7
  # This can be useful for multi-tenancy, i18n locale, timezone, any implicit
8
8
  # per-request attribute. See +ActiveSupport::CurrentAttributes+.
9
9
  #
10
+ # For multiple current attributes, pass an array of current attributes.
11
+ #
10
12
  # @example
11
13
  #
12
14
  # # in your initializer
13
15
  # require "sidekiq/middleware/current_attributes"
14
16
  # Sidekiq::CurrentAttributes.persist("Myapp::Current")
17
+ # # or multiple current attributes
18
+ # Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
15
19
  #
16
20
  module CurrentAttributes
17
21
  class Save
18
22
  include Sidekiq::ClientMiddleware
19
23
 
20
- def initialize(cattr)
21
- @strklass = cattr
24
+ def initialize(cattrs)
25
+ @cattrs = cattrs
22
26
  end
23
27
 
24
28
  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
29
+ @cattrs.each do |(key, strklass)|
30
+ if !job.has_key?(key)
31
+ attrs = strklass.constantize.attributes
32
+ # Retries can push the job N times, we don't
33
+ # want retries to reset cattr. #5692, #5090
34
+ job[key] = attrs if attrs.any?
31
35
  end
32
36
  end
33
37
  yield
@@ -37,22 +41,71 @@ module Sidekiq
37
41
  class Load
38
42
  include Sidekiq::ServerMiddleware
39
43
 
40
- def initialize(cattr)
41
- @strklass = cattr
44
+ def initialize(cattrs)
45
+ @cattrs = cattrs
42
46
  end
43
47
 
44
48
  def call(_, job, _, &block)
45
- if job.has_key?("cattr")
46
- @strklass.constantize.set(job["cattr"], &block)
47
- else
48
- yield
49
+ klass_attrs = {}
50
+
51
+ @cattrs.each do |(key, strklass)|
52
+ next unless job.has_key?(key)
53
+
54
+ klass_attrs[strklass.constantize] = job[key]
55
+ end
56
+
57
+ wrap(klass_attrs.to_a, &block)
58
+ end
59
+
60
+ private
61
+
62
+ def wrap(klass_attrs, &block)
63
+ klass, attrs = klass_attrs.shift
64
+ return block.call unless klass
65
+
66
+ retried = false
67
+
68
+ begin
69
+ klass.set(attrs) do
70
+ wrap(klass_attrs, &block)
71
+ end
72
+ rescue NoMethodError
73
+ raise if retried
74
+
75
+ # It is possible that the `CurrentAttributes` definition
76
+ # was changed before the job started processing.
77
+ attrs = attrs.select { |attr| klass.respond_to?(attr) }
78
+ retried = true
79
+ retry
49
80
  end
50
81
  end
51
82
  end
52
83
 
53
- def self.persist(klass, config = Sidekiq.default_configuration)
54
- config.client_middleware.add Save, klass.to_s
55
- config.server_middleware.add Load, klass.to_s
84
+ class << self
85
+ def persist(klass_or_array, config = Sidekiq.default_configuration)
86
+ cattrs = build_cattrs_hash(klass_or_array)
87
+
88
+ config.client_middleware.add Save, cattrs
89
+ config.server_middleware.add Load, cattrs
90
+ end
91
+
92
+ private
93
+
94
+ def build_cattrs_hash(klass_or_array)
95
+ if klass_or_array.is_a?(Array)
96
+ {}.tap do |hash|
97
+ klass_or_array.each_with_index do |klass, index|
98
+ hash[key_at(index)] = klass.to_s
99
+ end
100
+ end
101
+ else
102
+ {key_at(0) => klass_or_array.to_s}
103
+ end
104
+ end
105
+
106
+ def key_at(index)
107
+ (index == 0) ? "cattr" : "cattr_#{index}"
108
+ end
56
109
  end
57
110
  end
58
111
  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 = []
@@ -19,9 +19,9 @@ module Sidekiq
19
19
  total_size, items = conn.multi { |transaction|
20
20
  transaction.zcard(key)
21
21
  if rev
22
- transaction.zrevrange(key, starting, ending, withscores: true)
22
+ transaction.zrange(key, starting, ending, "REV", "withscores")
23
23
  else
24
- transaction.zrange(key, starting, ending, withscores: true)
24
+ transaction.zrange(key, starting, ending, "withscores")
25
25
  end
26
26
  }
27
27
  [current_page, total_size, 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
@@ -36,7 +36,7 @@ module Sidekiq
36
36
  @job = nil
37
37
  @thread = nil
38
38
  @reloader = Sidekiq.default_configuration[:reloader]
39
- @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(logger)
39
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
40
40
  @retrier = Sidekiq::JobRetry.new(capsule)
41
41
  end
42
42
 
@@ -58,6 +58,10 @@ module Sidekiq
58
58
  @thread.value if wait
59
59
  end
60
60
 
61
+ def stopping?
62
+ @done
63
+ end
64
+
61
65
  def start
62
66
  @thread ||= safe_thread("#{config.name}/processor", &method(:run))
63
67
  end
@@ -136,6 +140,7 @@ module Sidekiq
136
140
  klass = Object.const_get(job_hash["class"])
137
141
  inst = klass.new
138
142
  inst.jid = job_hash["jid"]
143
+ inst._context = self
139
144
  @retrier.local(inst, jobstr, queue) do
140
145
  yield inst
141
146
  end
@@ -146,6 +151,11 @@ module Sidekiq
146
151
  end
147
152
  end
148
153
 
154
+ IGNORE_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :never}
155
+ private_constant :IGNORE_SHUTDOWN_INTERRUPTS
156
+ ALLOW_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :immediate}
157
+ private_constant :ALLOW_SHUTDOWN_INTERRUPTS
158
+
149
159
  def process(uow)
150
160
  jobstr = uow.job
151
161
  queue = uow.queue_name
@@ -168,36 +178,40 @@ module Sidekiq
168
178
  end
169
179
 
170
180
  ack = false
171
- begin
172
- dispatch(job_hash, queue, jobstr) do |inst|
173
- config.server_middleware.invoke(inst, job_hash, queue) do
174
- execute_job(inst, job_hash["args"])
181
+ Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
182
+ Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
183
+ dispatch(job_hash, queue, jobstr) do |inst|
184
+ config.server_middleware.invoke(inst, job_hash, queue) do
185
+ execute_job(inst, job_hash["args"])
186
+ end
175
187
  end
188
+ ack = true
189
+ rescue Sidekiq::Shutdown
190
+ # Had to force kill this job because it didn't finish
191
+ # within the timeout. Don't acknowledge the work since
192
+ # we didn't properly finish it.
193
+ rescue Sidekiq::JobRetry::Skip => s
194
+ # Skip means we handled this error elsewhere. We don't
195
+ # need to log or report the error.
196
+ ack = true
197
+ raise s
198
+ rescue Sidekiq::JobRetry::Handled => h
199
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
200
+ # signals that we created a retry successfully. We can acknowledge the job.
201
+ ack = true
202
+ e = h.cause || h
203
+ handle_exception(e, {context: "Job raised exception", job: job_hash})
204
+ raise e
205
+ rescue Exception => ex
206
+ # Unexpected error! This is very bad and indicates an exception that got past
207
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
208
+ # so it can be rescued when using Sidekiq Pro.
209
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
210
+ raise ex
176
211
  end
177
- ack = true
178
- rescue Sidekiq::Shutdown
179
- # Had to force kill this job because it didn't finish
180
- # within the timeout. Don't acknowledge the work since
181
- # we didn't properly finish it.
182
- rescue Sidekiq::JobRetry::Handled => h
183
- # this is the common case: job raised error and Sidekiq::JobRetry::Handled
184
- # signals that we created a retry successfully. We can acknowlege the job.
185
- ack = true
186
- e = h.cause || h
187
- handle_exception(e, {context: "Job raised exception", job: job_hash})
188
- raise e
189
- rescue Exception => ex
190
- # Unexpected error! This is very bad and indicates an exception that got past
191
- # the retry subsystem (e.g. network partition). We won't acknowledge the job
192
- # so it can be rescued when using Sidekiq Pro.
193
- handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
194
- raise ex
195
212
  ensure
196
213
  if ack
197
- # We don't want a shutdown signal to interrupt job acknowledgment.
198
- Thread.handle_interrupt(Sidekiq::Shutdown => :never) do
199
- uow.acknowledge
200
- end
214
+ uow.acknowledge
201
215
  end
202
216
  end
203
217
  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
@@ -19,6 +20,10 @@ module Sidekiq
19
20
  def inspect
20
21
  "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
21
22
  end
23
+
24
+ def to_hash
25
+ {app: @app.class.name}
26
+ end
22
27
  end
23
28
 
24
29
  # By including the Options module, we allow AJs to directly control sidekiq features
@@ -38,14 +43,9 @@ module Sidekiq
38
43
  end
39
44
  end
40
45
 
41
- initializer "sidekiq.rails_logger" do
46
+ initializer "sidekiq.backtrace_cleaner" do
42
47
  Sidekiq.configure_server do |config|
43
- # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
44
- # it will appear in the Sidekiq console with all of the job context. See #5021 and
45
- # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
46
- unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
47
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
48
- end
48
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
49
49
  end
50
50
  end
51
51
 
@@ -56,6 +56,16 @@ module Sidekiq
56
56
  config.after_initialize do
57
57
  Sidekiq.configure_server do |config|
58
58
  config[:reloader] = Sidekiq::Rails::Reloader.new
59
+
60
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
61
+ # it will appear in the Sidekiq console with all of the job context.
62
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
63
+ if ::Rails.logger.respond_to?(:broadcast_to)
64
+ ::Rails.logger.broadcast_to(config.logger)
65
+ else
66
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
67
+ end
68
+ end
59
69
  end
60
70
  end
61
71
  end