sidekiq 7.3.9 → 8.1.7

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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +225 -0
  3. data/README.md +34 -14
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/sidekiqload +12 -10
  7. data/bin/webload +69 -0
  8. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +107 -58
  9. data/lib/generators/sidekiq/job_generator.rb +15 -3
  10. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  11. data/lib/sidekiq/api.rb +310 -83
  12. data/lib/sidekiq/capsule.rb +10 -7
  13. data/lib/sidekiq/cli.rb +31 -24
  14. data/lib/sidekiq/client.rb +36 -18
  15. data/lib/sidekiq/component.rb +45 -3
  16. data/lib/sidekiq/config.rb +35 -30
  17. data/lib/sidekiq/embedded.rb +2 -1
  18. data/lib/sidekiq/fetch.rb +1 -0
  19. data/lib/sidekiq/iterable_job.rb +1 -0
  20. data/lib/sidekiq/job/iterable.rb +46 -18
  21. data/lib/sidekiq/job.rb +18 -3
  22. data/lib/sidekiq/job_logger.rb +8 -6
  23. data/lib/sidekiq/job_retry.rb +45 -18
  24. data/lib/sidekiq/job_util.rb +5 -1
  25. data/lib/sidekiq/launcher.rb +22 -12
  26. data/lib/sidekiq/loader.rb +57 -0
  27. data/lib/sidekiq/logger.rb +25 -69
  28. data/lib/sidekiq/manager.rb +2 -2
  29. data/lib/sidekiq/metrics/query.rb +77 -46
  30. data/lib/sidekiq/metrics/shared.rb +8 -5
  31. data/lib/sidekiq/metrics/tracking.rb +12 -7
  32. data/lib/sidekiq/middleware/current_attributes.rb +11 -19
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/monitor.rb +4 -8
  35. data/lib/sidekiq/paginator.rb +18 -2
  36. data/lib/sidekiq/processor.rb +26 -18
  37. data/lib/sidekiq/profiler.rb +73 -0
  38. data/lib/sidekiq/rails.rb +46 -67
  39. data/lib/sidekiq/redis_client_adapter.rb +6 -3
  40. data/lib/sidekiq/redis_connection.rb +16 -5
  41. data/lib/sidekiq/ring_buffer.rb +1 -0
  42. data/lib/sidekiq/scheduled.rb +6 -7
  43. data/lib/sidekiq/test_api.rb +331 -0
  44. data/lib/sidekiq/testing/inline.rb +2 -30
  45. data/lib/sidekiq/testing.rb +2 -333
  46. data/lib/sidekiq/transaction_aware_client.rb +13 -5
  47. data/lib/sidekiq/tui/controls.rb +53 -0
  48. data/lib/sidekiq/tui/filtering.rb +53 -0
  49. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  50. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  51. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  52. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  53. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  54. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  55. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  56. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  57. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  58. data/lib/sidekiq/tui/tabs.rb +15 -0
  59. data/lib/sidekiq/tui.rb +390 -0
  60. data/lib/sidekiq/version.rb +2 -2
  61. data/lib/sidekiq/web/action.rb +146 -83
  62. data/lib/sidekiq/web/application.rb +372 -328
  63. data/lib/sidekiq/web/config.rb +117 -0
  64. data/lib/sidekiq/web/helpers.rb +102 -44
  65. data/lib/sidekiq/web/router.rb +61 -77
  66. data/lib/sidekiq/web.rb +65 -151
  67. data/lib/sidekiq.rb +13 -1
  68. data/sidekiq.gemspec +9 -8
  69. data/web/assets/images/logo.png +0 -0
  70. data/web/assets/images/status.png +0 -0
  71. data/web/assets/javascripts/application.js +43 -27
  72. data/web/assets/javascripts/base-charts.js +30 -16
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/dashboard.js +1 -1
  75. data/web/assets/javascripts/metrics.js +16 -34
  76. data/web/assets/stylesheets/style.css +776 -0
  77. data/web/locales/ar.yml +3 -1
  78. data/web/locales/cs.yml +2 -0
  79. data/web/locales/da.yml +2 -0
  80. data/web/locales/de.yml +2 -0
  81. data/web/locales/el.yml +2 -0
  82. data/web/locales/en.yml +8 -0
  83. data/web/locales/es.yml +25 -2
  84. data/web/locales/fa.yml +3 -1
  85. data/web/locales/fr.yml +3 -1
  86. data/web/locales/gd.yml +13 -1
  87. data/web/locales/he.yml +3 -1
  88. data/web/locales/hi.yml +36 -0
  89. data/web/locales/it.yml +9 -0
  90. data/web/locales/ja.yml +2 -0
  91. data/web/locales/ko.yml +2 -0
  92. data/web/locales/lt.yml +2 -0
  93. data/web/locales/nb.yml +2 -0
  94. data/web/locales/nl.yml +2 -0
  95. data/web/locales/pl.yml +2 -0
  96. data/web/locales/{pt-br.yml → pt-BR.yml} +3 -1
  97. data/web/locales/pt.yml +2 -0
  98. data/web/locales/ru.yml +2 -0
  99. data/web/locales/sv.yml +2 -0
  100. data/web/locales/ta.yml +83 -47
  101. data/web/locales/tr.yml +2 -0
  102. data/web/locales/uk.yml +7 -5
  103. data/web/locales/ur.yml +3 -1
  104. data/web/locales/vi.yml +2 -0
  105. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -73
  106. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -1
  107. data/web/views/_footer.html.erb +32 -0
  108. data/web/views/_job_info.html.erb +115 -0
  109. data/web/views/_metrics_period_select.html.erb +15 -0
  110. data/web/views/_nav.html.erb +45 -0
  111. data/web/views/_paging.html.erb +26 -0
  112. data/web/views/_poll_link.html.erb +4 -0
  113. data/web/views/{_summary.erb → _summary.html.erb} +16 -16
  114. data/web/views/busy.html.erb +155 -0
  115. data/web/views/dashboard.html.erb +104 -0
  116. data/web/views/dead.html.erb +38 -0
  117. data/web/views/filtering.html.erb +6 -0
  118. data/web/views/layout.html.erb +26 -0
  119. data/web/views/metrics.html.erb +85 -0
  120. data/web/views/metrics_for_job.html.erb +58 -0
  121. data/web/views/morgue.html.erb +69 -0
  122. data/web/views/profiles.html.erb +43 -0
  123. data/web/views/queue.html.erb +57 -0
  124. data/web/views/queues.html.erb +46 -0
  125. data/web/views/retries.html.erb +77 -0
  126. data/web/views/retry.html.erb +39 -0
  127. data/web/views/scheduled.html.erb +64 -0
  128. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  129. metadata +65 -47
  130. data/lib/sidekiq/web/csrf_protection.rb +0 -183
  131. data/web/assets/stylesheets/application-dark.css +0 -147
  132. data/web/assets/stylesheets/application-rtl.css +0 -163
  133. data/web/assets/stylesheets/application.css +0 -759
  134. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  135. data/web/assets/stylesheets/bootstrap.css +0 -5
  136. data/web/views/_footer.erb +0 -34
  137. data/web/views/_job_info.erb +0 -105
  138. data/web/views/_metrics_period_select.erb +0 -12
  139. data/web/views/_nav.erb +0 -52
  140. data/web/views/_paging.erb +0 -25
  141. data/web/views/_poll_link.erb +0 -4
  142. data/web/views/_status.erb +0 -4
  143. data/web/views/busy.erb +0 -148
  144. data/web/views/dashboard.erb +0 -108
  145. data/web/views/dead.erb +0 -34
  146. data/web/views/filtering.erb +0 -6
  147. data/web/views/layout.erb +0 -42
  148. data/web/views/metrics.erb +0 -90
  149. data/web/views/metrics_for_job.erb +0 -59
  150. data/web/views/morgue.erb +0 -74
  151. data/web/views/queue.erb +0 -55
  152. data/web/views/queues.erb +0 -44
  153. data/web/views/retries.erb +0 -79
  154. data/web/views/retry.erb +0 -34
  155. data/web/views/scheduled.erb +0 -56
data/lib/sidekiq/cli.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  $stdout.sync = true
4
4
 
5
- require "yaml"
6
- require "singleton"
7
5
  require "optparse"
8
6
  require "erb"
9
7
  require "fileutils"
@@ -17,7 +15,6 @@ require "sidekiq/launcher"
17
15
  module Sidekiq # :nodoc:
18
16
  class CLI
19
17
  include Sidekiq::Component
20
- include Singleton unless $TESTING
21
18
 
22
19
  attr_accessor :launcher
23
20
  attr_accessor :environment
@@ -31,6 +28,10 @@ module Sidekiq # :nodoc:
31
28
  validate!
32
29
  end
33
30
 
31
+ def self.instance
32
+ @instance ||= new
33
+ end
34
+
34
35
  def jruby?
35
36
  defined?(::JRUBY_VERSION)
36
37
  end
@@ -47,7 +48,7 @@ module Sidekiq # :nodoc:
47
48
  logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
48
49
 
49
50
  self_read, self_write = IO.pipe
50
- sigs = %w[INT TERM TTIN TSTP]
51
+ sigs = %w[INT TERM INFO TTIN TSTP]
51
52
  # USR1 and USR2 don't work on the JVM
52
53
  sigs << "USR2" if Sidekiq.pro? && !jruby?
53
54
  sigs.each do |sig|
@@ -74,7 +75,7 @@ module Sidekiq # :nodoc:
74
75
  # fire startup and start multithreading.
75
76
  info = @config.redis_info
76
77
  ver = Gem::Version.new(info["redis_version"])
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
+ raise "You are connected to Redis #{ver}, Sidekiq requires Redis 7.0.0 or greater" if ver < Gem::Version.new("7.0.0")
78
79
 
79
80
  maxmemory_policy = info["maxmemory_policy"]
80
81
  if maxmemory_policy != "noeviction" && maxmemory_policy != ""
@@ -208,14 +209,25 @@ module Sidekiq # :nodoc:
208
209
  cli.logger.warn "<no backtrace available>"
209
210
  end
210
211
  end
212
+ },
213
+ "INFO" => ->(cli) {
214
+ cli.logger.error { "DEPRECATED: the INFO signal does not work on Linux, use TTIN instead." }
215
+ Thread.list.each do |thread|
216
+ cli.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
217
+ if thread.backtrace
218
+ cli.logger.warn thread.backtrace.join("\n")
219
+ else
220
+ cli.logger.warn "<no backtrace available>"
221
+ end
222
+ end
211
223
  }
212
224
  }
213
- UNHANDLED_SIGNAL_HANDLER = ->(cli) { cli.logger.info "No signal handler registered, ignoring" }
214
- SIGNAL_HANDLERS.default = UNHANDLED_SIGNAL_HANDLER
215
225
 
216
226
  def handle_signal(sig)
217
227
  logger.debug "Got #{sig} signal"
218
- SIGNAL_HANDLERS[sig].call(self)
228
+ hndlr = SIGNAL_HANDLERS[sig]
229
+ hndlr ? hndlr.call(self) :
230
+ logger.warn("No #{sig} signal handler registered, ignoring")
219
231
  end
220
232
 
221
233
  private
@@ -298,29 +310,18 @@ module Sidekiq # :nodoc:
298
310
 
299
311
  if File.directory?(@config[:require])
300
312
  require "rails"
301
- if ::Rails::VERSION::MAJOR < 6
302
- warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
313
+ if ::Rails::VERSION::MAJOR < 7
314
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 7+"
303
315
  end
304
316
  require "sidekiq/rails"
305
317
  require File.expand_path("#{@config[:require]}/config/environment.rb")
306
- @config[:tag] ||= default_tag
318
+ @config[:tag] ||= default_tag(::Rails.root)
307
319
  else
308
320
  require @config[:require]
321
+ @config[:tag] ||= default_tag
309
322
  end
310
323
  end
311
324
 
312
- def default_tag
313
- dir = ::Rails.root
314
- name = File.basename(dir)
315
- prevdir = File.dirname(dir) # Capistrano release directory?
316
- if name.to_i != 0 && prevdir
317
- if File.basename(prevdir) == "releases"
318
- return File.basename(File.dirname(prevdir))
319
- end
320
- end
321
- name
322
- end
323
-
324
325
  def validate!
325
326
  if !File.exist?(@config[:require]) ||
326
327
  (File.directory?(@config[:require]) && !File.exist?("#{@config[:require]}/config/application.rb"))
@@ -395,12 +396,18 @@ module Sidekiq # :nodoc:
395
396
  end
396
397
 
397
398
  def initialize_logger
398
- @config.logger.level = ::Logger::DEBUG if @config[:verbose]
399
+ if @config[:verbose] || ENV["DEBUG_INVOCATION"] == "1"
400
+ # DEBUG_INVOCATION is a systemd-ism triggered by
401
+ # RestartMode=debug. We turn on debugging when the
402
+ # sidekiq process crashes and is restarted with this flag.
403
+ @config.logger.level = ::Logger::DEBUG
404
+ end
399
405
  end
400
406
 
401
407
  def parse_config(path)
402
408
  erb = ERB.new(File.read(path), trim_mode: "-")
403
409
  erb.filename = File.expand_path(path)
410
+ require "yaml"
404
411
  opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
405
412
 
406
413
  if opts.respond_to? :deep_symbolize_keys!
@@ -67,9 +67,7 @@ module Sidekiq
67
67
  c.pipelined do |p|
68
68
  p.hsetnx(key, "cancelled", Time.now.to_i)
69
69
  p.hget(key, "cancelled")
70
- p.expire(key, Sidekiq::Job::Iterable::STATE_TTL)
71
- # TODO When Redis 7.2 is required
72
- # p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
70
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
73
71
  end
74
72
  end
75
73
  result.to_i
@@ -119,6 +117,14 @@ module Sidekiq
119
117
  # larger than 1000 but YMMV based on network quality, size of job args, etc.
120
118
  # A large number of jobs can cause a bit of Redis command processing latency.
121
119
  #
120
+ # Accepts an `:at` option to schedule the jobs for future execution. It
121
+ # accepts either a single Numeric timestamp (or seconds-from-now) applied
122
+ # to every job, or an Array of Numeric values with the same size as `args`
123
+ # to schedule each job at its corresponding time.
124
+ #
125
+ # Accepts an additional `:spread_interval` option (in seconds) to randomly spread
126
+ # the jobs schedule times over the specified interval.
127
+ #
122
128
  # Takes the same arguments as #push except that args is expected to be
123
129
  # an Array of Arrays. All other keys are duplicated for each job. Each job
124
130
  # is run through the client middleware pipeline and each job gets its own Job ID
@@ -131,15 +137,28 @@ module Sidekiq
131
137
  # push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
132
138
  #
133
139
  def push_bulk(items)
134
- batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
135
140
  args = items["args"]
136
- at = items.delete("at")
141
+ at = items.delete("at") || items.delete(:at)
137
142
  raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
138
143
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
139
144
 
140
145
  jid = items.delete("jid")
141
146
  raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
142
147
 
148
+ spread_interval = items.delete(:spread_interval) || items.delete("spread_interval")
149
+ raise ArgumentError, "Jobs 'spread_interval' must be a positive Numeric" if spread_interval && (!spread_interval.is_a?(Numeric) || spread_interval <= 0)
150
+ raise ArgumentError, "Only one of 'at' or 'spread_interval' can be provided" if at && spread_interval
151
+
152
+ if !at && spread_interval
153
+ # Do not use spread interval smaller than pooling interval.
154
+ spread_interval = [spread_interval, 5].max
155
+ now = Time.now.to_f
156
+ at = args.map { now + rand * spread_interval }
157
+ end
158
+
159
+ # Use a smaller batch size by default for scheduled jobs since adding to sorted sets is more costly.
160
+ batch_size = items.delete(:batch_size) || items.delete("batch_size") || (at ? 100 : 1_000)
161
+
143
162
  normed = normalize_item(items)
144
163
  slice_index = 0
145
164
  result = args.each_slice(batch_size).flat_map do |slice|
@@ -266,22 +285,21 @@ module Sidekiq
266
285
  if payloads.first.key?("at")
267
286
  conn.zadd("schedule", payloads.flat_map { |hash|
268
287
  at = hash["at"].to_s
269
- # ActiveJob sets this but the job has not been enqueued yet
270
- hash.delete("enqueued_at")
271
- # TODO: Use hash.except("at") when support for Ruby 2.7 is dropped
272
- hash = hash.dup
273
- hash.delete("at")
288
+ # ActiveJob sets enqueued_at but the job has not been enqueued yet
289
+ hash = hash.except("enqueued_at", "at")
274
290
  [at, Sidekiq.dump_json(hash)]
275
291
  })
276
292
  else
277
- queue = payloads.first["queue"]
278
- now = Time.now.to_f
279
- to_push = payloads.map { |entry|
280
- entry["enqueued_at"] = now
281
- Sidekiq.dump_json(entry)
282
- }
283
- conn.sadd("queues", [queue])
284
- conn.lpush("queue:#{queue}", to_push)
293
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) # milliseconds since the epoch
294
+ grouped_queues = payloads.group_by { |job| job["queue"] }
295
+ conn.sadd("queues", grouped_queues.keys)
296
+ grouped_queues.each do |queue, grouped_payloads|
297
+ to_push = grouped_payloads.map { |entry|
298
+ entry["enqueued_at"] = now
299
+ Sidekiq.dump_json(entry)
300
+ }
301
+ conn.lpush("queue:#{queue}", to_push)
302
+ end
285
303
  end
286
304
  end
287
305
  end
@@ -1,11 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
+ # Ruby's default thread priority is 0, which uses 100ms time slices.
5
+ # This can lead to some surprising thread starvation; if using a lot of
6
+ # CPU-heavy concurrency, it may take several seconds before a Thread gets
7
+ # on the CPU.
8
+ #
9
+ # Negative priorities lower the timeslice by half, so -1 = 50ms, -2 = 25ms, etc.
10
+ # With more frequent timeslices, we reduce the risk of unintentional timeouts
11
+ # and starvation.
12
+ #
13
+ # Customize like so:
14
+ #
15
+ # Sidekiq.configure_server do |cfg|
16
+ # cfg.thread_priority = 0
17
+ # end
18
+ #
19
+ DEFAULT_THREAD_PRIORITY = -1
20
+
4
21
  ##
5
- # Sidekiq::Component assumes a config instance is available at @config
22
+ # Sidekiq::Component provides a set of utility methods depending only
23
+ # on Sidekiq::Config. It assumes a config instance is available at @config.
6
24
  module Component # :nodoc:
7
25
  attr_reader :config
8
26
 
27
+ # This is epoch milliseconds, appropriate for persistence
28
+ def real_ms
29
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
30
+ end
31
+
32
+ # used for time difference and relative comparisons, not persistence.
33
+ def mono_ms
34
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
35
+ end
36
+
9
37
  def watchdog(last_words)
10
38
  yield
11
39
  rescue Exception => ex
@@ -13,11 +41,11 @@ module Sidekiq
13
41
  raise ex
14
42
  end
15
43
 
16
- def safe_thread(name, &block)
44
+ def safe_thread(name, priority: nil, &block)
17
45
  Thread.new do
18
46
  Thread.current.name = "sidekiq.#{name}"
19
47
  watchdog(name, &block)
20
- end
48
+ end.tap { |t| t.priority = (priority || config.thread_priority || DEFAULT_THREAD_PRIORITY) }
21
49
  end
22
50
 
23
51
  def logger
@@ -29,6 +57,9 @@ module Sidekiq
29
57
  end
30
58
 
31
59
  def tid
60
+ # We XOR with PID to ensure Thread IDs changes after fork.
61
+ # I'm unclear why we don't multiply the two values to better guarantee
62
+ # a unique value but it's been this way for quite a while now. #3685
32
63
  Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
33
64
  end
34
65
 
@@ -86,5 +117,16 @@ module Sidekiq
86
117
  end.join(", ")
87
118
  }>"
88
119
  end
120
+
121
+ def default_tag(dir = Dir.pwd)
122
+ name = File.basename(dir)
123
+ prevdir = File.dirname(dir) # Capistrano release directory?
124
+ if name.to_i != 0 && prevdir
125
+ if File.basename(prevdir) == "releases"
126
+ return File.basename(File.dirname(prevdir))
127
+ end
128
+ end
129
+ name
130
+ end
89
131
  end
90
132
  end
@@ -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
+ redis_idle_timeout: nil
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,15 @@ module Sidekiq
135
146
  @redis_config = @redis_config.merge(hash)
136
147
  end
137
148
 
149
+ def reap_idle_redis_connections(timeout = 60)
150
+ self[:redis_idle_timeout] = timeout
151
+ end
152
+
138
153
  def redis_pool
139
154
  Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
140
155
  end
141
156
 
142
- private def local_redis_pool
157
+ def local_redis_pool
143
158
  # this is our internal client/housekeeping pool. each capsule has its
144
159
  # own pool for executing threads.
145
160
  @redis ||= new_redis_pool(10, "internal")
@@ -240,7 +255,7 @@ module Sidekiq
240
255
  # Register a proc to handle any error which occurs within the Sidekiq process.
241
256
  #
242
257
  # Sidekiq.configure_server do |config|
243
- # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
258
+ # config.error_handlers << proc {|ex,ctx_hash,config| MyErrorService.notify(ex, ctx_hash) }
244
259
  # end
245
260
  #
246
261
  # The default error handler logs errors to @logger.
@@ -249,7 +264,7 @@ module Sidekiq
249
264
  end
250
265
 
251
266
  # Register a block to run at a point in the Sidekiq lifecycle.
252
- # :startup, :quiet or :shutdown are valid events.
267
+ # :startup, :quiet, :shutdown, or :exit are valid events.
253
268
  #
254
269
  # Sidekiq.configure_server do |config|
255
270
  # config.on(:shutdown) do
@@ -282,24 +297,14 @@ module Sidekiq
282
297
  @logger = logger
283
298
  end
284
299
 
285
- private def parameter_size(handler)
286
- target = handler.is_a?(Proc) ? handler : handler.method(:call)
287
- target.parameters.size
288
- end
289
-
290
300
  # INTERNAL USE ONLY
291
301
  def handle_exception(ex, ctx = {})
292
- if @options[:error_handlers].size == 0
293
- p ["!!!!!", ex]
302
+ if @options[:error_handlers].empty?
303
+ logger.error { "No error handlers configured, logging exception directly" }
304
+ logger.error { ex }
294
305
  end
295
306
  @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
307
+ handler.call(ex, ctx, self)
303
308
  rescue Exception => e
304
309
  l = logger
305
310
  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"
data/lib/sidekiq/fetch.rb CHANGED
@@ -7,6 +7,7 @@ require "sidekiq/capsule"
7
7
  module Sidekiq # :nodoc:
8
8
  class BasicFetch
9
9
  include Sidekiq::Component
10
+
10
11
  # We want the fetch operation to timeout every few seconds so the thread
11
12
  # can check if the process is shutting down.
12
13
  TIMEOUT = 2
@@ -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