sidekiq 7.3.10 → 8.0.10

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +135 -4
  3. data/README.md +16 -13
  4. data/bin/sidekiqload +10 -10
  5. data/bin/webload +69 -0
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +104 -58
  7. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  8. data/lib/sidekiq/api.rb +152 -44
  9. data/lib/sidekiq/capsule.rb +10 -6
  10. data/lib/sidekiq/cli.rb +30 -23
  11. data/lib/sidekiq/client.rb +28 -17
  12. data/lib/sidekiq/component.rb +42 -3
  13. data/lib/sidekiq/config.rb +32 -22
  14. data/lib/sidekiq/embedded.rb +2 -1
  15. data/lib/sidekiq/iterable_job.rb +1 -0
  16. data/lib/sidekiq/job/iterable.rb +46 -18
  17. data/lib/sidekiq/job.rb +2 -2
  18. data/lib/sidekiq/job_logger.rb +8 -6
  19. data/lib/sidekiq/job_retry.rb +33 -10
  20. data/lib/sidekiq/job_util.rb +5 -1
  21. data/lib/sidekiq/launcher.rb +20 -10
  22. data/lib/sidekiq/loader.rb +57 -0
  23. data/lib/sidekiq/logger.rb +25 -69
  24. data/lib/sidekiq/manager.rb +0 -1
  25. data/lib/sidekiq/metrics/query.rb +71 -45
  26. data/lib/sidekiq/metrics/shared.rb +8 -5
  27. data/lib/sidekiq/metrics/tracking.rb +12 -7
  28. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  29. data/lib/sidekiq/monitor.rb +4 -8
  30. data/lib/sidekiq/paginator.rb +8 -1
  31. data/lib/sidekiq/processor.rb +21 -14
  32. data/lib/sidekiq/profiler.rb +73 -0
  33. data/lib/sidekiq/rails.rb +46 -67
  34. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  35. data/lib/sidekiq/redis_connection.rb +2 -2
  36. data/lib/sidekiq/scheduled.rb +4 -4
  37. data/lib/sidekiq/testing.rb +3 -3
  38. data/lib/sidekiq/version.rb +2 -2
  39. data/lib/sidekiq/web/action.rb +146 -83
  40. data/lib/sidekiq/web/application.rb +361 -330
  41. data/lib/sidekiq/web/config.rb +120 -0
  42. data/lib/sidekiq/web/helpers.rb +58 -36
  43. data/lib/sidekiq/web/router.rb +60 -76
  44. data/lib/sidekiq/web.rb +51 -156
  45. data/lib/sidekiq.rb +6 -1
  46. data/sidekiq.gemspec +6 -6
  47. data/web/assets/images/logo.png +0 -0
  48. data/web/assets/images/status.png +0 -0
  49. data/web/assets/javascripts/application.js +42 -26
  50. data/web/assets/javascripts/base-charts.js +30 -16
  51. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  52. data/web/assets/javascripts/dashboard.js +1 -1
  53. data/web/assets/javascripts/metrics.js +16 -34
  54. data/web/assets/stylesheets/style.css +776 -0
  55. data/web/locales/ar.yml +2 -0
  56. data/web/locales/cs.yml +2 -0
  57. data/web/locales/da.yml +2 -0
  58. data/web/locales/de.yml +2 -0
  59. data/web/locales/el.yml +2 -0
  60. data/web/locales/en.yml +7 -0
  61. data/web/locales/es.yml +25 -2
  62. data/web/locales/fa.yml +2 -0
  63. data/web/locales/fr.yml +3 -1
  64. data/web/locales/gd.yml +2 -0
  65. data/web/locales/he.yml +2 -0
  66. data/web/locales/hi.yml +2 -0
  67. data/web/locales/it.yml +9 -0
  68. data/web/locales/ja.yml +2 -0
  69. data/web/locales/ko.yml +2 -0
  70. data/web/locales/lt.yml +2 -0
  71. data/web/locales/nb.yml +2 -0
  72. data/web/locales/nl.yml +2 -0
  73. data/web/locales/pl.yml +2 -0
  74. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  75. data/web/locales/pt.yml +2 -0
  76. data/web/locales/ru.yml +2 -0
  77. data/web/locales/sv.yml +2 -0
  78. data/web/locales/ta.yml +2 -0
  79. data/web/locales/tr.yml +2 -0
  80. data/web/locales/uk.yml +7 -5
  81. data/web/locales/ur.yml +2 -0
  82. data/web/locales/vi.yml +2 -0
  83. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  84. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
  85. data/web/views/_footer.erb +31 -33
  86. data/web/views/_job_info.erb +91 -89
  87. data/web/views/_metrics_period_select.erb +13 -10
  88. data/web/views/_nav.erb +14 -21
  89. data/web/views/_paging.erb +22 -21
  90. data/web/views/_poll_link.erb +2 -2
  91. data/web/views/_summary.erb +16 -16
  92. data/web/views/busy.erb +123 -125
  93. data/web/views/dashboard.erb +62 -66
  94. data/web/views/dead.erb +31 -27
  95. data/web/views/filtering.erb +4 -4
  96. data/web/views/layout.erb +12 -28
  97. data/web/views/metrics.erb +76 -82
  98. data/web/views/metrics_for_job.erb +45 -46
  99. data/web/views/morgue.erb +65 -70
  100. data/web/views/profiles.erb +43 -0
  101. data/web/views/queue.erb +54 -52
  102. data/web/views/queues.erb +43 -41
  103. data/web/views/retries.erb +70 -75
  104. data/web/views/retry.erb +32 -27
  105. data/web/views/scheduled.erb +63 -55
  106. data/web/views/scheduled_job_info.erb +3 -3
  107. metadata +24 -41
  108. data/web/assets/stylesheets/application-dark.css +0 -147
  109. data/web/assets/stylesheets/application-rtl.css +0 -163
  110. data/web/assets/stylesheets/application.css +0 -759
  111. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  112. data/web/assets/stylesheets/bootstrap.css +0 -5
  113. data/web/views/_status.erb +0 -4
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
-
5
- require "set"
6
4
  require "sidekiq/redis_connection"
7
5
 
8
6
  module Sidekiq
@@ -19,16 +17,16 @@ module Sidekiq
19
17
  poll_interval_average: nil,
20
18
  average_scheduled_poll_interval: 5,
21
19
  on_complex_arguments: :raise,
22
- iteration: {
23
- max_job_runtime: nil,
24
- retry_backoff: 0
25
- },
20
+ # if the Iterable job runs longer than this value (in seconds), then the job
21
+ # will be interrupted after the current iteration and re-enqueued at the back of the queue
22
+ max_iteration_runtime: nil,
26
23
  error_handlers: [],
27
24
  death_handlers: [],
28
25
  lifecycle_events: {
29
26
  startup: [],
30
27
  quiet: [],
31
28
  shutdown: [],
29
+ exit: [],
32
30
  # triggers when we fire the first heartbeat on startup OR repairing a network partition
33
31
  heartbeat: [],
34
32
  # triggers on EVERY heartbeat call, every 10 seconds
@@ -37,16 +35,28 @@ module Sidekiq
37
35
  dead_max_jobs: 10_000,
38
36
  dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
39
37
  reloader: proc { |&block| block.call },
40
- backtrace_cleaner: ->(backtrace) { backtrace }
38
+ backtrace_cleaner: ->(backtrace) { backtrace },
39
+ logged_job_attributes: ["bid", "tags"],
40
+ reap_connections: nil # TODO Enable by default
41
41
  }
42
42
 
43
43
  ERROR_HANDLER = ->(ex, ctx, cfg = Sidekiq.default_configuration) {
44
- l = cfg.logger
45
- l.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
46
- l.warn("#{ex.class.name}: #{ex.message}")
47
- unless ex.backtrace.nil?
48
- backtrace = cfg[:backtrace_cleaner].call(ex.backtrace)
49
- l.warn(backtrace.join("\n"))
44
+ Sidekiq::Context.with(ctx) do
45
+ dev = cfg[:environment] == "development"
46
+ fancy = dev && $stdout.tty? # 🎩
47
+ # Weird logic here but we want to show the backtrace in local
48
+ # development or if verbose logging is enabled.
49
+ #
50
+ # `full_message` contains the error class, message and backtrace
51
+ # `detailed_message` contains the error class and message
52
+ #
53
+ # Absolutely terrible API names. Not useful at all to have two
54
+ # methods with similar but obscure names.
55
+ if dev || cfg.logger.debug?
56
+ cfg.logger.info { ex.full_message(highlight: fancy) }
57
+ else
58
+ cfg.logger.info { ex.detailed_message(highlight: fancy) }
59
+ end
50
60
  end
51
61
  }
52
62
 
@@ -60,6 +70,7 @@ module Sidekiq
60
70
 
61
71
  def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
62
72
  attr_reader :capsules
73
+ attr_accessor :thread_priority
63
74
 
64
75
  def inspect
65
76
  "#<#{self.class.name} @options=#{
@@ -135,11 +146,16 @@ module Sidekiq
135
146
  @redis_config = @redis_config.merge(hash)
136
147
  end
137
148
 
149
+ def reap_idle_redis_connections(timeout = nil)
150
+ self[:reap_connections] = timeout
151
+ end
152
+ alias_method :reap, :reap_idle_redis_connections
153
+
138
154
  def redis_pool
139
155
  Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
140
156
  end
141
157
 
142
- private def local_redis_pool
158
+ def local_redis_pool
143
159
  # this is our internal client/housekeeping pool. each capsule has its
144
160
  # own pool for executing threads.
145
161
  @redis ||= new_redis_pool(10, "internal")
@@ -249,7 +265,7 @@ module Sidekiq
249
265
  end
250
266
 
251
267
  # Register a block to run at a point in the Sidekiq lifecycle.
252
- # :startup, :quiet or :shutdown are valid events.
268
+ # :startup, :quiet, :shutdown, or :exit are valid events.
253
269
  #
254
270
  # Sidekiq.configure_server do |config|
255
271
  # config.on(:shutdown) do
@@ -293,13 +309,7 @@ module Sidekiq
293
309
  p ["!!!!!", ex]
294
310
  end
295
311
  @options[:error_handlers].each do |handler|
296
- if parameter_size(handler) == 2
297
- # TODO Remove in 8.0
298
- logger.info { "DEPRECATION: Sidekiq exception handlers now take three arguments, see #{handler}" }
299
- handler.call(ex, {_config: self}.merge(ctx))
300
- else
301
- handler.call(ex, ctx, self)
302
- end
312
+ handler.call(ex, ctx, self)
303
313
  rescue Exception => e
304
314
  l = logger
305
315
  l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
@@ -34,6 +34,7 @@ module Sidekiq
34
34
  private
35
35
 
36
36
  def housekeeping
37
+ @config[:tag] ||= default_tag
37
38
  logger.info "Running in #{RUBY_DESCRIPTION}"
38
39
  logger.info Sidekiq::LICENSE
39
40
  logger.info "Upgrade to Sidekiq Pro for more features and support: https://sidekiq.org" unless defined?(::Sidekiq::Pro)
@@ -42,7 +43,7 @@ module Sidekiq
42
43
  # fire startup and start multithreading.
43
44
  info = config.redis_info
44
45
  ver = Gem::Version.new(info["redis_version"])
45
- raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
46
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
46
47
 
47
48
  maxmemory_policy = info["maxmemory_policy"]
48
49
  if maxmemory_policy != "noeviction"
@@ -46,6 +46,7 @@ module Sidekiq
46
46
  # def on_start
47
47
  # def on_resume
48
48
  # def on_stop
49
+ # def on_cancel
49
50
  # def on_complete
50
51
  # def around_iteration
51
52
  #
@@ -32,8 +32,14 @@ module Sidekiq
32
32
  @_runtime = 0
33
33
  @_args = nil
34
34
  @_cancelled = nil
35
+ @current_object = nil
35
36
  end
36
37
 
38
+ # Access to the current object while iterating.
39
+ # This value is not reset so the latest element is
40
+ # explicitly available to cleanup/complete callbacks.
41
+ attr_reader :current_object
42
+
37
43
  def arguments
38
44
  @_args
39
45
  end
@@ -49,14 +55,12 @@ module Sidekiq
49
55
  def cancel!
50
56
  return @_cancelled if cancelled?
51
57
 
52
- key = "it-#{jid}"
58
+ key = iteration_key
53
59
  _, result, _ = Sidekiq.redis do |c|
54
60
  c.pipelined do |p|
55
61
  p.hsetnx(key, "cancelled", Time.now.to_i)
56
62
  p.hget(key, "cancelled")
57
- # TODO When Redis 7.2 is required
58
- # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
59
- p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
63
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
60
64
  end
61
65
  end
62
66
  @_cancelled = result.to_i
@@ -66,6 +70,10 @@ module Sidekiq
66
70
  @_cancelled
67
71
  end
68
72
 
73
+ def cursor
74
+ @_cursor.freeze
75
+ end
76
+
69
77
  # A hook to override that will be called when the job starts iterating.
70
78
  #
71
79
  # It is called only once, for the first time.
@@ -93,6 +101,11 @@ module Sidekiq
93
101
  def on_stop
94
102
  end
95
103
 
104
+ # A hook to override that will be called when the job is cancelled.
105
+ #
106
+ def on_cancel
107
+ end
108
+
96
109
  # A hook to override that will be called when the job finished iterating.
97
110
  #
98
111
  def on_complete
@@ -130,7 +143,7 @@ module Sidekiq
130
143
  fetch_previous_iteration_state
131
144
 
132
145
  @_executions += 1
133
- @_start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
146
+ @_start_time = mono_now
134
147
 
135
148
  enumerator = build_enumerator(*args, cursor: @_cursor)
136
149
  unless enumerator
@@ -164,7 +177,7 @@ module Sidekiq
164
177
  private
165
178
 
166
179
  def is_cancelled?
167
- @_cancelled = Sidekiq.redis { |c| c.hget("it-#{jid}", "cancelled") }
180
+ @_cancelled = Sidekiq.redis { |c| c.hget(iteration_key, "cancelled") }
168
181
  end
169
182
 
170
183
  def fetch_previous_iteration_state
@@ -184,34 +197,40 @@ module Sidekiq
184
197
 
185
198
  def iterate_with_enumerator(enumerator, arguments)
186
199
  if is_cancelled?
200
+ on_cancel
187
201
  logger.info { "Job cancelled" }
188
202
  return true
189
203
  end
190
204
 
191
205
  time_limit = Sidekiq.default_configuration[:timeout]
192
206
  found_record = false
193
- state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
207
+ state_flushed_at = mono_now
194
208
 
195
209
  enumerator.each do |object, cursor|
196
210
  found_record = true
197
211
  @_cursor = cursor
212
+ @current_object = object
198
213
 
199
- is_interrupted = interrupted?
200
- if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - state_flushed_at >= STATE_FLUSH_INTERVAL || is_interrupted
214
+ interrupt_job = interrupted? || should_interrupt?
215
+ if mono_now - state_flushed_at >= STATE_FLUSH_INTERVAL || interrupt_job
201
216
  _, _, cancelled = flush_state
202
- state_flushed_at = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
217
+ state_flushed_at = mono_now
203
218
  if cancelled
204
219
  @_cancelled = true
220
+ on_cancel
205
221
  logger.info { "Job cancelled" }
206
222
  return true
207
223
  end
208
224
  end
209
225
 
210
- return false if is_interrupted
226
+ return false if interrupt_job
211
227
 
212
- verify_iteration_time(time_limit, object) do
228
+ verify_iteration_time(time_limit) do
213
229
  around_iteration do
214
230
  each_iteration(object, *arguments)
231
+ rescue Exception
232
+ flush_state
233
+ raise
215
234
  end
216
235
  end
217
236
  end
@@ -219,16 +238,16 @@ module Sidekiq
219
238
  logger.debug("Enumerator found nothing to iterate!") unless found_record
220
239
  true
221
240
  ensure
222
- @_runtime += (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @_start_time)
241
+ @_runtime += (mono_now - @_start_time)
223
242
  end
224
243
 
225
- def verify_iteration_time(time_limit, object)
226
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
244
+ def verify_iteration_time(time_limit)
245
+ start = mono_now
227
246
  yield
228
- finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
247
+ finish = mono_now
229
248
  total = finish - start
230
249
  if total > time_limit
231
- logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d) when processing `%s`. This can lead to job processing problems during deploys" % [total, time_limit, object] }
250
+ logger.warn { "Iteration took longer (%.2f) than Sidekiq's shutdown timeout (%d). This can lead to job processing problems during deploys" % [total, time_limit] }
232
251
  end
233
252
  end
234
253
 
@@ -254,6 +273,11 @@ module Sidekiq
254
273
  end
255
274
  end
256
275
 
276
+ def should_interrupt?
277
+ max_iteration_runtime = Sidekiq.default_configuration[:max_iteration_runtime]
278
+ max_iteration_runtime && (mono_now - @_start_time > max_iteration_runtime)
279
+ end
280
+
257
281
  def flush_state
258
282
  key = iteration_key
259
283
  state = {
@@ -265,7 +289,7 @@ module Sidekiq
265
289
  Sidekiq.redis do |conn|
266
290
  conn.multi do |pipe|
267
291
  pipe.hset(key, state)
268
- pipe.expire(key, STATE_TTL)
292
+ pipe.expire(key, STATE_TTL, "nx")
269
293
  pipe.hget(key, "cancelled")
270
294
  end
271
295
  end
@@ -289,6 +313,10 @@ module Sidekiq
289
313
  raise "Unexpected thrown value: #{completed.inspect}"
290
314
  end
291
315
  end
316
+
317
+ def mono_now
318
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
319
+ end
292
320
  end
293
321
  end
294
322
  end
data/lib/sidekiq/job.rb CHANGED
@@ -248,9 +248,9 @@ module Sidekiq
248
248
  end
249
249
  alias_method :perform_sync, :perform_inline
250
250
 
251
- def perform_bulk(args, batch_size: 1_000)
251
+ def perform_bulk(args, **options)
252
252
  client = @klass.build_client
253
- client.push_bulk(@opts.merge("class" => @klass, "args" => args, :batch_size => batch_size))
253
+ client.push_bulk(@opts.merge({"class" => @klass, "args" => args}, options))
254
254
  end
255
255
 
256
256
  # +interval+ must be a timestamp, numeric or something that acts
@@ -26,16 +26,18 @@ module Sidekiq
26
26
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
27
27
  # attribute to expose the underlying thing.
28
28
  h = {
29
- class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
30
- jid: job_hash["jid"]
29
+ jid: job_hash["jid"],
30
+ class: job_hash["wrapped"] || job_hash["class"]
31
31
  }
32
- h[:bid] = job_hash["bid"] if job_hash.has_key?("bid")
33
- h[:tags] = job_hash["tags"] if job_hash.has_key?("tags")
32
+
33
+ @config[:logged_job_attributes].each do |attr|
34
+ h[attr.to_sym] = job_hash[attr] if job_hash.has_key?(attr)
35
+ end
34
36
 
35
37
  Thread.current[:sidekiq_context] = h
36
38
  level = job_hash["log_level"]
37
- if level && @logger.respond_to?(:log_at)
38
- @logger.log_at(level, &block)
39
+ if level
40
+ @logger.with_level(level, &block)
39
41
  else
40
42
  yield
41
43
  end
@@ -139,6 +139,10 @@ module Sidekiq
139
139
 
140
140
  private
141
141
 
142
+ def now_ms
143
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
144
+ end
145
+
142
146
  # Note that +jobinst+ can be nil here if an error is raised before we can
143
147
  # instantiate the job instance. All access must be guarded and
144
148
  # best effort.
@@ -149,17 +153,17 @@ module Sidekiq
149
153
 
150
154
  m = exception_message(exception)
151
155
  if m.respond_to?(:scrub!)
152
- m.force_encoding("utf-8")
156
+ m.force_encoding(Encoding::UTF_8)
153
157
  m.scrub!
154
158
  end
155
159
 
156
160
  msg["error_message"] = m
157
161
  msg["error_class"] = exception.class.name
158
162
  count = if msg["retry_count"]
159
- msg["retried_at"] = Time.now.to_f
163
+ msg["retried_at"] = now_ms
160
164
  msg["retry_count"] += 1
161
165
  else
162
- msg["failed_at"] = Time.now.to_f
166
+ msg["failed_at"] = now_ms
163
167
  msg["retry_count"] = 0
164
168
  end
165
169
 
@@ -177,19 +181,21 @@ module Sidekiq
177
181
  return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
178
182
 
179
183
  rf = msg["retry_for"]
180
- return retries_exhausted(jobinst, msg, exception) if rf && ((msg["failed_at"] + rf) < Time.now.to_f)
184
+ return retries_exhausted(jobinst, msg, exception) if rf && (time_for(msg["failed_at"]) + rf) < Time.now
181
185
 
182
186
  strategy, delay = delay_for(jobinst, count, exception, msg)
183
187
  case strategy
184
188
  when :discard
185
- return # poof!
189
+ msg["discarded_at"] = now_ms
190
+
191
+ return run_death_handlers(msg, exception)
186
192
  when :kill
187
193
  return retries_exhausted(jobinst, msg, exception)
188
194
  end
189
195
 
190
196
  # Logging here can break retries if the logging device raises ENOSPC #3979
191
197
  # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
192
- jitter = rand(10) * (count + 1)
198
+ jitter = rand(10 * (count + 1))
193
199
  retry_at = Time.now.to_f + delay + jitter
194
200
  payload = Sidekiq.dump_json(msg)
195
201
  redis do |conn|
@@ -197,6 +203,14 @@ module Sidekiq
197
203
  end
198
204
  end
199
205
 
206
+ def time_for(item)
207
+ if item.is_a?(Float)
208
+ Time.at(item)
209
+ else
210
+ Time.at(item / 1000, item % 1000)
211
+ end
212
+ end
213
+
200
214
  # returns (strategy, seconds)
201
215
  def delay_for(jobinst, count, exception, msg)
202
216
  rv = begin
@@ -243,13 +257,22 @@ module Sidekiq
243
257
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
244
258
  end
245
259
 
246
- return if rv == :discard # poof!
247
- send_to_morgue(msg) unless msg["dead"] == false
260
+ discarded = msg["dead"] == false || rv == :discard
261
+
262
+ if discarded
263
+ msg["discarded_at"] = now_ms
264
+ else
265
+ send_to_morgue(msg)
266
+ end
267
+
268
+ run_death_handlers(msg, exception)
269
+ end
248
270
 
271
+ def run_death_handlers(job, exception)
249
272
  @capsule.config.death_handlers.each do |handler|
250
- handler.call(msg, exception)
273
+ handler.call(job, exception)
251
274
  rescue => e
252
- handle_exception(e, {context: "Error calling death handler", job: msg})
275
+ handle_exception(e, {context: "Error calling death handler", job: job})
253
276
  end
254
277
  end
255
278
 
@@ -58,10 +58,14 @@ module Sidekiq
58
58
  item["class"] = item["class"].to_s
59
59
  item["queue"] = item["queue"].to_s
60
60
  item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
61
- item["created_at"] ||= Time.now.to_f
61
+ item["created_at"] ||= now_in_millis
62
62
  item
63
63
  end
64
64
 
65
+ def now_in_millis
66
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
67
+ end
68
+
65
69
  def normalized_hash(item_class)
66
70
  if item_class.is_a?(Class)
67
71
  raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
@@ -68,6 +68,7 @@ module Sidekiq
68
68
  stoppers.each(&:join)
69
69
 
70
70
  clear_heartbeat
71
+ fire_event(:exit, reverse: true)
71
72
  end
72
73
 
73
74
  def stopping?
@@ -81,7 +82,7 @@ module Sidekiq
81
82
  ❤
82
83
  end
83
84
 
84
- private unless $TESTING
85
+ private
85
86
 
86
87
  BEAT_PAUSE = 10
87
88
 
@@ -141,6 +142,12 @@ module Sidekiq
141
142
  key = identity
142
143
  fails = procd = 0
143
144
 
145
+ idle = config[:reap_connections]
146
+ if idle
147
+ config.capsules.each_value { |cap| cap.local_redis_pool.reap(idle, &:close) }
148
+ config.local_redis_pool.reap(idle, &:close)
149
+ end
150
+
144
151
  begin
145
152
  flush_stats
146
153
 
@@ -213,11 +220,11 @@ module Sidekiq
213
220
  # Log a warning if it's a disaster.
214
221
  if RTT_READINGS.all? { |x| x > RTT_WARNING_LEVEL }
215
222
  logger.warn <<~EOM
216
- Your Redis network connection is performing extremely poorly.
223
+ Your Redis network connection appears to be performing poorly.
217
224
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
218
- Ensure Redis is running in the same AZ or datacenter as Sidekiq.
219
- If these values are close to 100,000, that means your Sidekiq process may be
220
- CPU-saturated; reduce your concurrency and/or see https://github.com/sidekiq/sidekiq/discussions/5039
225
+ Ensure Redis is running in the same AZ or datacenter as Sidekiq and that
226
+ your Sidekiq process is not CPU-saturated; reduce your concurrency and/or
227
+ see https://github.com/sidekiq/sidekiq/discussions/5039
221
228
  EOM
222
229
  RTT_READINGS.reset
223
230
  end
@@ -251,8 +258,15 @@ module Sidekiq
251
258
  "pid" => ::Process.pid,
252
259
  "tag" => @config[:tag] || "",
253
260
  "concurrency" => @config.total_concurrency,
261
+ "capsules" => @config.capsules.each_with_object({}) { |(name, cap), memo|
262
+ memo[name] = cap.to_h
263
+ },
264
+ #####
265
+ # TODO deprecated, remove in 9.0
266
+ # This data is now found in the `capsules` element above
254
267
  "queues" => @config.capsules.values.flat_map { |cap| cap.queues }.uniq,
255
- "weights" => to_weights,
268
+ "weights" => @config.capsules.values.map(&:weights),
269
+ #####
256
270
  "labels" => @config[:labels].to_a,
257
271
  "identity" => identity,
258
272
  "version" => Sidekiq::VERSION,
@@ -260,10 +274,6 @@ module Sidekiq
260
274
  }
261
275
  end
262
276
 
263
- def to_weights
264
- @config.capsules.values.map(&:weights)
265
- end
266
-
267
277
  def to_json
268
278
  # this data changes infrequently so dump it to a string
269
279
  # now so we don't need to dump it every heartbeat.
@@ -0,0 +1,57 @@
1
+ module Sidekiq
2
+ require "sidekiq/component"
3
+
4
+ class Loader
5
+ include Sidekiq::Component
6
+
7
+ def initialize(cfg = Sidekiq.default_configuration)
8
+ @config = cfg
9
+ @load_hooks = Hash.new { |h, k| h[k] = [] }
10
+ @loaded = Set.new
11
+ @lock = Mutex.new
12
+ end
13
+
14
+ # Declares a block that will be executed when a Sidekiq component is fully
15
+ # loaded. If the component has already loaded, the block is executed
16
+ # immediately.
17
+ #
18
+ # Sidekiq.loader.on_load(:api) do
19
+ # # extend the sidekiq API
20
+ # end
21
+ #
22
+ def on_load(name, &block)
23
+ # we don't want to hold the lock while calling the block
24
+ to_run = nil
25
+
26
+ @lock.synchronize do
27
+ if @loaded.include?(name)
28
+ to_run = block
29
+ else
30
+ @load_hooks[name] << block
31
+ end
32
+ end
33
+
34
+ to_run&.call
35
+ nil
36
+ end
37
+
38
+ # Executes all blocks registered to +name+ via on_load.
39
+ #
40
+ # Sidekiq.loader.run_load_hooks(:api)
41
+ #
42
+ # In the case of the above example, it will execute all hooks registered for +:api+.
43
+ #
44
+ def run_load_hooks(name)
45
+ hks = @lock.synchronize do
46
+ @loaded << name
47
+ @load_hooks.delete(name)
48
+ end
49
+
50
+ hks&.each do |blk|
51
+ blk.call
52
+ rescue => ex
53
+ handle_exception(ex, hook: name)
54
+ end
55
+ end
56
+ end
57
+ end