sidekiq 7.3.10 → 8.0.0.beta1

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +21 -12
  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 +5 -5
  7. data/lib/sidekiq/api.rb +108 -36
  8. data/lib/sidekiq/capsule.rb +1 -1
  9. data/lib/sidekiq/cli.rb +15 -19
  10. data/lib/sidekiq/client.rb +13 -16
  11. data/lib/sidekiq/component.rb +30 -2
  12. data/lib/sidekiq/config.rb +18 -15
  13. data/lib/sidekiq/embedded.rb +1 -0
  14. data/lib/sidekiq/fetch.rb +0 -1
  15. data/lib/sidekiq/job/iterable.rb +2 -4
  16. data/lib/sidekiq/job_retry.rb +2 -2
  17. data/lib/sidekiq/job_util.rb +5 -1
  18. data/lib/sidekiq/launcher.rb +1 -1
  19. data/lib/sidekiq/logger.rb +6 -10
  20. data/lib/sidekiq/manager.rb +0 -1
  21. data/lib/sidekiq/metrics/query.rb +1 -3
  22. data/lib/sidekiq/middleware/current_attributes.rb +5 -17
  23. data/lib/sidekiq/middleware/i18n.rb +0 -2
  24. data/lib/sidekiq/paginator.rb +8 -1
  25. data/lib/sidekiq/processor.rb +21 -14
  26. data/lib/sidekiq/profiler.rb +59 -0
  27. data/lib/sidekiq/redis_client_adapter.rb +0 -1
  28. data/lib/sidekiq/redis_connection.rb +3 -14
  29. data/lib/sidekiq/ring_buffer.rb +0 -1
  30. data/lib/sidekiq/testing.rb +2 -2
  31. data/lib/sidekiq/transaction_aware_client.rb +5 -13
  32. data/lib/sidekiq/version.rb +2 -2
  33. data/lib/sidekiq/web/action.rb +101 -85
  34. data/lib/sidekiq/web/application.rb +339 -333
  35. data/lib/sidekiq/web/config.rb +116 -0
  36. data/lib/sidekiq/web/helpers.rb +41 -16
  37. data/lib/sidekiq/web/router.rb +60 -76
  38. data/lib/sidekiq/web.rb +51 -156
  39. data/sidekiq.gemspec +5 -4
  40. data/web/assets/javascripts/application.js +6 -13
  41. data/web/assets/javascripts/base-charts.js +30 -18
  42. data/web/assets/javascripts/metrics.js +1 -1
  43. data/web/assets/stylesheets/style.css +750 -0
  44. data/web/locales/ar.yml +1 -0
  45. data/web/locales/cs.yml +1 -0
  46. data/web/locales/da.yml +1 -0
  47. data/web/locales/de.yml +1 -0
  48. data/web/locales/el.yml +1 -0
  49. data/web/locales/en.yml +6 -0
  50. data/web/locales/es.yml +24 -2
  51. data/web/locales/fa.yml +1 -0
  52. data/web/locales/fr.yml +1 -0
  53. data/web/locales/gd.yml +1 -0
  54. data/web/locales/he.yml +1 -0
  55. data/web/locales/hi.yml +1 -0
  56. data/web/locales/it.yml +1 -0
  57. data/web/locales/ja.yml +1 -0
  58. data/web/locales/ko.yml +1 -0
  59. data/web/locales/lt.yml +1 -0
  60. data/web/locales/nb.yml +1 -0
  61. data/web/locales/nl.yml +1 -0
  62. data/web/locales/pl.yml +1 -0
  63. data/web/locales/{pt-br.yml → pt-BR.yml} +2 -1
  64. data/web/locales/pt.yml +1 -0
  65. data/web/locales/ru.yml +1 -0
  66. data/web/locales/sv.yml +1 -0
  67. data/web/locales/ta.yml +1 -0
  68. data/web/locales/tr.yml +1 -0
  69. data/web/locales/uk.yml +1 -0
  70. data/web/locales/ur.yml +1 -0
  71. data/web/locales/vi.yml +1 -0
  72. data/web/locales/{zh-cn.yml → zh-CN.yml} +85 -73
  73. data/web/locales/{zh-tw.yml → zh-TW.yml} +2 -1
  74. data/web/views/_footer.erb +31 -33
  75. data/web/views/_job_info.erb +91 -89
  76. data/web/views/_metrics_period_select.erb +1 -1
  77. data/web/views/_nav.erb +14 -21
  78. data/web/views/_paging.erb +23 -21
  79. data/web/views/_poll_link.erb +2 -2
  80. data/web/views/_summary.erb +16 -16
  81. data/web/views/busy.erb +124 -122
  82. data/web/views/dashboard.erb +61 -66
  83. data/web/views/dead.erb +31 -27
  84. data/web/views/filtering.erb +3 -3
  85. data/web/views/layout.erb +5 -21
  86. data/web/views/metrics.erb +83 -80
  87. data/web/views/metrics_for_job.erb +39 -42
  88. data/web/views/morgue.erb +61 -70
  89. data/web/views/profiles.erb +43 -0
  90. data/web/views/queue.erb +54 -52
  91. data/web/views/queues.erb +43 -41
  92. data/web/views/retries.erb +66 -75
  93. data/web/views/retry.erb +32 -27
  94. data/web/views/scheduled.erb +58 -54
  95. data/web/views/scheduled_job_info.erb +1 -1
  96. metadata +31 -36
  97. data/web/assets/stylesheets/application-dark.css +0 -147
  98. data/web/assets/stylesheets/application-rtl.css +0 -163
  99. data/web/assets/stylesheets/application.css +0 -759
  100. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  101. data/web/assets/stylesheets/bootstrap.css +0 -5
  102. data/web/views/_status.erb +0 -4
@@ -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
@@ -266,22 +264,21 @@ module Sidekiq
266
264
  if payloads.first.key?("at")
267
265
  conn.zadd("schedule", payloads.flat_map { |hash|
268
266
  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")
267
+ # ActiveJob sets enqueued_at but the job has not been enqueued yet
268
+ hash = hash.except("enqueued_at", "at")
274
269
  [at, Sidekiq.dump_json(hash)]
275
270
  })
276
271
  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)
272
+ now = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) # milliseconds since the epoch
273
+ grouped_queues = payloads.group_by { |job| job["queue"] }
274
+ conn.sadd("queues", grouped_queues.keys)
275
+ grouped_queues.each do |queue, grouped_payloads|
276
+ to_push = grouped_payloads.map { |entry|
277
+ entry["enqueued_at"] = now
278
+ Sidekiq.dump_json(entry)
279
+ }
280
+ conn.lpush("queue:#{queue}", to_push)
281
+ end
285
282
  end
286
283
  end
287
284
  end
@@ -1,6 +1,23 @@
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
22
  # Sidekiq::Component assumes a config instance is available at @config
6
23
  module Component # :nodoc:
@@ -13,11 +30,11 @@ module Sidekiq
13
30
  raise ex
14
31
  end
15
32
 
16
- def safe_thread(name, &block)
33
+ def safe_thread(name, priority: nil, &block)
17
34
  Thread.new do
18
35
  Thread.current.name = "sidekiq.#{name}"
19
36
  watchdog(name, &block)
20
- end
37
+ end.tap { |t| t.priority = (priority || config.thread_priority || DEFAULT_THREAD_PRIORITY) }
21
38
  end
22
39
 
23
40
  def logger
@@ -86,5 +103,16 @@ module Sidekiq
86
103
  end.join(", ")
87
104
  }>"
88
105
  end
106
+
107
+ def default_tag(dir = Dir.pwd)
108
+ name = File.basename(dir)
109
+ prevdir = File.dirname(dir) # Capistrano release directory?
110
+ if name.to_i != 0 && prevdir
111
+ if File.basename(prevdir) == "releases"
112
+ return File.basename(File.dirname(prevdir))
113
+ end
114
+ end
115
+ name
116
+ end
89
117
  end
90
118
  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
@@ -41,12 +39,22 @@ module Sidekiq
41
39
  }
42
40
 
43
41
  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"))
42
+ Sidekiq::Context.with(ctx) do
43
+ dev = cfg[:environment] == "development"
44
+ fancy = dev && $stdout.tty? # 🎩
45
+ # Weird logic here but we want to show the backtrace in local
46
+ # development or if verbose logging is enabled.
47
+ #
48
+ # `full_message` contains the error class, message and backtrace
49
+ # `detailed_message` contains the error class and message
50
+ #
51
+ # Absolutely terrible API names. Not useful at all to have two
52
+ # methods with similar but obscure names.
53
+ if dev || cfg.logger.debug?
54
+ cfg.logger.info { ex.full_message(highlight: fancy) }
55
+ else
56
+ cfg.logger.info { ex.detailed_message(highlight: fancy) }
57
+ end
50
58
  end
51
59
  }
52
60
 
@@ -60,6 +68,7 @@ module Sidekiq
60
68
 
61
69
  def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!, :dig
62
70
  attr_reader :capsules
71
+ attr_accessor :thread_priority
63
72
 
64
73
  def inspect
65
74
  "#<#{self.class.name} @options=#{
@@ -293,13 +302,7 @@ module Sidekiq
293
302
  p ["!!!!!", ex]
294
303
  end
295
304
  @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
305
+ handler.call(ex, ctx, self)
303
306
  rescue Exception => e
304
307
  l = logger
305
308
  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)
data/lib/sidekiq/fetch.rb CHANGED
@@ -7,7 +7,6 @@ require "sidekiq/capsule"
7
7
  module Sidekiq # :nodoc:
8
8
  class BasicFetch
9
9
  include Sidekiq::Component
10
-
11
10
  # We want the fetch operation to timeout every few seconds so the thread
12
11
  # can check if the process is shutting down.
13
12
  TIMEOUT = 2
@@ -54,9 +54,7 @@ module Sidekiq
54
54
  c.pipelined do |p|
55
55
  p.hsetnx(key, "cancelled", Time.now.to_i)
56
56
  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)
57
+ p.expire(key, Sidekiq::Job::Iterable::STATE_TTL, "nx")
60
58
  end
61
59
  end
62
60
  @_cancelled = result.to_i
@@ -265,7 +263,7 @@ module Sidekiq
265
263
  Sidekiq.redis do |conn|
266
264
  conn.multi do |pipe|
267
265
  pipe.hset(key, state)
268
- pipe.expire(key, STATE_TTL)
266
+ pipe.expire(key, STATE_TTL, "nx")
269
267
  pipe.hget(key, "cancelled")
270
268
  end
271
269
  end
@@ -149,7 +149,7 @@ module Sidekiq
149
149
 
150
150
  m = exception_message(exception)
151
151
  if m.respond_to?(:scrub!)
152
- m.force_encoding("utf-8")
152
+ m.force_encoding(Encoding::UTF_8)
153
153
  m.scrub!
154
154
  end
155
155
 
@@ -189,7 +189,7 @@ module Sidekiq
189
189
 
190
190
  # Logging here can break retries if the logging device raises ENOSPC #3979
191
191
  # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
192
- jitter = rand(10) * (count + 1)
192
+ jitter = rand(10 * (count + 1))
193
193
  retry_at = Time.now.to_f + delay + jitter
194
194
  payload = Sidekiq.dump_json(msg)
195
195
  redis do |conn|
@@ -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)
@@ -81,7 +81,7 @@ module Sidekiq
81
81
 
82
82
  end
83
83
 
84
- private unless $TESTING
84
+ private
85
85
 
86
86
  BEAT_PAUSE = 10
87
87
 
@@ -81,13 +81,9 @@ module Sidekiq
81
81
  Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
82
82
  end
83
83
 
84
- def ctx
85
- Sidekiq::Context.current
86
- end
87
-
88
- def format_context
89
- if ctx.any?
90
- " " + ctx.compact.map { |k, v|
84
+ def format_context(ctxt = Sidekiq::Context.current)
85
+ if ctxt.size > 0
86
+ ctxt.map { |k, v|
91
87
  case v
92
88
  when Array
93
89
  "#{k}=#{v.join(",")}"
@@ -101,13 +97,13 @@ module Sidekiq
101
97
 
102
98
  class Pretty < Base
103
99
  def call(severity, time, program_name, message)
104
- "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
100
+ "#{time.utc.iso8601(3)} pid=#{::Process.pid} tid=#{tid} #{format_context} #{severity}: #{message}\n"
105
101
  end
106
102
  end
107
103
 
108
104
  class WithoutTimestamp < Pretty
109
105
  def call(severity, time, program_name, message)
110
- "pid=#{::Process.pid} tid=#{tid}#{format_context} #{severity}: #{message}\n"
106
+ "pid=#{::Process.pid} tid=#{tid} #{format_context} #{severity}: #{message}\n"
111
107
  end
112
108
  end
113
109
 
@@ -120,7 +116,7 @@ module Sidekiq
120
116
  lvl: severity,
121
117
  msg: message
122
118
  }
123
- c = ctx
119
+ c = Sidekiq::Context.current
124
120
  hash["ctx"] = c unless c.empty?
125
121
 
126
122
  Sidekiq.dump_json(hash) << "\n"
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sidekiq/processor"
4
- require "set"
5
4
 
6
5
  module Sidekiq
7
6
  ##
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq"
4
3
  require "date"
5
- require "set"
6
-
4
+ require "sidekiq"
7
5
  require "sidekiq/metrics/shared"
8
6
 
9
7
  module Sidekiq
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_job/arguments"
3
4
  require "active_support/current_attributes"
4
5
 
5
6
  module Sidekiq
@@ -20,6 +21,8 @@ module Sidekiq
20
21
  # Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
21
22
  #
22
23
  module CurrentAttributes
24
+ Serializer = ::ActiveJob::Arguments
25
+
23
26
  class Save
24
27
  include Sidekiq::ClientMiddleware
25
28
 
@@ -33,26 +36,11 @@ module Sidekiq
33
36
  attrs = strklass.constantize.attributes
34
37
  # Retries can push the job N times, we don't
35
38
  # want retries to reset cattr. #5692, #5090
36
- if attrs.any?
37
- # Older rails has a bug that `CurrentAttributes#attributes` always returns
38
- # the same hash instance. We need to dup it to avoid being accidentally mutated.
39
- job[key] = if returns_same_object?
40
- attrs.dup
41
- else
42
- attrs
43
- end
44
- end
39
+ job[key] = Serializer.serialize(attrs) if attrs.any?
45
40
  end
46
41
  end
47
42
  yield
48
43
  end
49
-
50
- private
51
-
52
- def returns_same_object?
53
- ActiveSupport::VERSION::MAJOR < 8 ||
54
- (ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
55
- end
56
44
  end
57
45
 
58
46
  class Load
@@ -68,7 +56,7 @@ module Sidekiq
68
56
  @cattrs.each do |(key, strklass)|
69
57
  next unless job.has_key?(key)
70
58
 
71
- klass_attrs[strklass.constantize] = job[key]
59
+ klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
72
60
  end
73
61
 
74
62
  wrap(klass_attrs.to_a, &block)
@@ -11,7 +11,6 @@ module Sidekiq::Middleware::I18n
11
11
  # to be sent to Sidekiq.
12
12
  class Client
13
13
  include Sidekiq::ClientMiddleware
14
-
15
14
  def call(_jobclass, job, _queue, _redis)
16
15
  job["locale"] ||= I18n.locale
17
16
  yield
@@ -21,7 +20,6 @@ module Sidekiq::Middleware::I18n
21
20
  # Pull the msg locale out and set the current thread to use it.
22
21
  class Server
23
22
  include Sidekiq::ServerMiddleware
24
-
25
23
  def call(_jobclass, job, _queue, &block)
26
24
  I18n.with_locale(job.fetch("locale", I18n.default_locale), &block)
27
25
  end
@@ -17,7 +17,14 @@ module Sidekiq
17
17
  ending = starting + page_size - 1
18
18
 
19
19
  Sidekiq.redis do |conn|
20
- type = conn.type(key)
20
+ # horrible, think you can make this cleaner?
21
+ type = TYPE_CACHE[key]
22
+ if type
23
+ elsif key.start_with?("queue:")
24
+ type = TYPE_CACHE[key] = "list"
25
+ else
26
+ type = TYPE_CACHE[key] = conn.type(key)
27
+ end
21
28
  rev = opts && opts[:reverse]
22
29
 
23
30
  case type
@@ -3,6 +3,7 @@
3
3
  require "sidekiq/fetch"
4
4
  require "sidekiq/job_logger"
5
5
  require "sidekiq/job_retry"
6
+ require "sidekiq/profiler"
6
7
 
7
8
  module Sidekiq
8
9
  ##
@@ -66,7 +67,7 @@ module Sidekiq
66
67
  @thread ||= safe_thread("#{config.name}/processor", &method(:run))
67
68
  end
68
69
 
69
- private unless $TESTING
70
+ private
70
71
 
71
72
  def run
72
73
  # By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
@@ -112,13 +113,17 @@ module Sidekiq
112
113
  def handle_fetch_exception(ex)
113
114
  unless @down
114
115
  @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
115
- logger.error("Error fetching job: #{ex}")
116
116
  handle_exception(ex)
117
117
  end
118
118
  sleep(1)
119
119
  nil
120
120
  end
121
121
 
122
+ def profile(job, &block)
123
+ return yield unless job["profile"]
124
+ Sidekiq::Profiler.new(config).call(job, &block)
125
+ end
126
+
122
127
  def dispatch(job_hash, queue, jobstr)
123
128
  # since middleware can mutate the job hash
124
129
  # we need to clone it to report the original
@@ -132,17 +137,19 @@ module Sidekiq
132
137
  @retrier.global(jobstr, queue) do
133
138
  @job_logger.call(job_hash, queue) do
134
139
  stats(jobstr, queue) do
135
- # Rails 5 requires a Reloader to wrap code execution. In order to
136
- # constantize the worker and instantiate an instance, we have to call
137
- # the Reloader. It handles code loading, db connection management, etc.
138
- # Effectively this block denotes a "unit of work" to Rails.
139
- @reloader.call do
140
- klass = Object.const_get(job_hash["class"])
141
- instance = klass.new
142
- instance.jid = job_hash["jid"]
143
- instance._context = self
144
- @retrier.local(instance, jobstr, queue) do
145
- yield instance
140
+ profile(job_hash) do
141
+ # Rails 5 requires a Reloader to wrap code execution. In order to
142
+ # constantize the worker and instantiate an instance, we have to call
143
+ # the Reloader. It handles code loading, db connection management, etc.
144
+ # Effectively this block denotes a "unit of work" to Rails.
145
+ @reloader.call do
146
+ klass = Object.const_get(job_hash["class"])
147
+ instance = klass.new
148
+ instance.jid = job_hash["jid"]
149
+ instance._context = self
150
+ @retrier.local(instance, jobstr, queue) do
151
+ yield instance
152
+ end
146
153
  end
147
154
  end
148
155
  end
@@ -165,7 +172,6 @@ module Sidekiq
165
172
  begin
166
173
  job_hash = Sidekiq.load_json(jobstr)
167
174
  rescue => ex
168
- handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
169
175
  now = Time.now.to_f
170
176
  redis do |conn|
171
177
  conn.multi do |xa|
@@ -174,6 +180,7 @@ module Sidekiq
174
180
  xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
175
181
  end
176
182
  end
183
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
177
184
  return uow.acknowledge
178
185
  end
179
186
 
@@ -0,0 +1,59 @@
1
+ require "fileutils"
2
+ require "sidekiq/component"
3
+
4
+ module Sidekiq
5
+ # Allows the user to profile jobs running in production.
6
+ # See details in the Profiling wiki page.
7
+ class Profiler
8
+ EXPIRY = 86400 # 1 day
9
+ DEFAULT_OPTIONS = {
10
+ mode: :wall
11
+ }
12
+
13
+ include Sidekiq::Component
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ def call(job, &block)
19
+ return yield unless job["profile"]
20
+
21
+ token = job["profile"]
22
+ type = job["class"]
23
+ jid = job["jid"]
24
+ started_at = Time.now
25
+ options = DEFAULT_OPTIONS.merge((job["profiler_options"] || {}).transform_keys!(&:to_sym))
26
+
27
+ rundata = {
28
+ started_at: started_at.to_i,
29
+ token: token,
30
+ type: type,
31
+ jid: jid,
32
+ # .gz extension tells Vernier to compress the data
33
+ filename: "#{token}-#{type}-#{jid}-#{started_at.strftime("%Y%m%d-%H%M%S")}.json.gz"
34
+ }
35
+
36
+ require "vernier"
37
+ begin
38
+ a = Time.now
39
+ rc = Vernier.profile(**options.merge(out: rundata[:filename]), &block)
40
+ b = Time.now
41
+
42
+ # Failed jobs will raise an exception on previous line and skip this
43
+ # block. Only successful jobs will persist profile data to Redis.
44
+ key = "#{token}-#{jid}"
45
+ data = File.read(rundata[:filename])
46
+ redis do |conn|
47
+ conn.multi do |m|
48
+ m.zadd("profiles", Time.now.to_f + EXPIRY, key)
49
+ m.hset(key, rundata.merge(elapsed: (b - a), data: data, size: data.bytesize))
50
+ m.expire(key, EXPIRY)
51
+ end
52
+ end
53
+ rc
54
+ ensure
55
+ FileUtils.rm_f(rundata[:filename])
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "redis_client"
5
4
  require "redis_client/decorator"
6
5
 
@@ -10,8 +10,6 @@ module Sidekiq
10
10
  def create(options = {})
11
11
  symbolized_options = deep_symbolize_keys(options)
12
12
  symbolized_options[:url] ||= determine_redis_provider
13
- symbolized_options[:password] = wrap(symbolized_options[:password]) if symbolized_options.key?(:password)
14
- symbolized_options[:sentinel_password] = wrap(symbolized_options[:sentinel_password]) if symbolized_options.key?(:sentinel_password)
15
13
 
16
14
  logger = symbolized_options.delete(:logger)
17
15
  logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
@@ -40,15 +38,6 @@ module Sidekiq
40
38
 
41
39
  private
42
40
 
43
- # Wrap hard-coded passwords in a Proc to avoid logging the value
44
- def wrap(pwd)
45
- if pwd.is_a?(String)
46
- ->(username) { pwd }
47
- else
48
- pwd
49
- end
50
- end
51
-
52
41
  def deep_symbolize_keys(object)
53
42
  case object
54
43
  when Hash
@@ -68,14 +57,14 @@ module Sidekiq
68
57
  # Deep clone so we can muck with these options all we want and exclude
69
58
  # params from dump-and-load that may contain objects that Marshal is
70
59
  # unable to safely dump.
71
- keys = options.keys - [:logger, :ssl_params, :password, :sentinel_password]
60
+ keys = options.keys - [:logger, :ssl_params]
72
61
  scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
73
62
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
74
63
  uri.password = redacted
75
64
  scrubbed_options[:url] = uri.to_s
76
65
  end
77
- scrubbed_options[:password] = redacted if options.key?(:password)
78
- scrubbed_options[:sentinel_password] = redacted if options.key?(:sentinel_password)
66
+ scrubbed_options[:password] = redacted if scrubbed_options[:password]
67
+ scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
79
68
  scrubbed_options[:sentinels]&.each do |sentinel|
80
69
  if sentinel.is_a?(String)
81
70
  if (uri = URI(sentinel)) && uri.password
@@ -6,7 +6,6 @@ module Sidekiq
6
6
  class RingBuffer
7
7
  include Enumerable
8
8
  extend Forwardable
9
-
10
9
  def_delegators :@buf, :[], :each, :size
11
10
 
12
11
  def initialize(size, default = 0)
@@ -87,7 +87,7 @@ module Sidekiq
87
87
  if Sidekiq::Testing.fake?
88
88
  payloads.each do |job|
89
89
  job = Sidekiq.load_json(Sidekiq.dump_json(job))
90
- job["enqueued_at"] = Time.now.to_f unless job["at"]
90
+ job["enqueued_at"] = ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond) unless job["at"]
91
91
  Queues.push(job["queue"], job["class"], job)
92
92
  end
93
93
  true
@@ -329,6 +329,6 @@ module Sidekiq
329
329
  end
330
330
  end
331
331
 
332
- if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
332
+ if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING # rubocop:disable Style/GlobalVars
333
333
  warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
334
334
  end
@@ -7,12 +7,6 @@ module Sidekiq
7
7
  class TransactionAwareClient
8
8
  def initialize(pool: nil, config: nil)
9
9
  @redis_client = Client.new(pool: pool, config: config)
10
- @transaction_backend =
11
- if ActiveRecord.version >= Gem::Version.new("7.2")
12
- ActiveRecord.method(:after_all_transactions_commit)
13
- else
14
- AfterCommitEverywhere.method(:after_commit)
15
- end
16
10
  end
17
11
 
18
12
  def batching?
@@ -26,7 +20,7 @@ module Sidekiq
26
20
  # pre-allocate the JID so we can return it immediately and
27
21
  # save it to the database as part of the transaction.
28
22
  item["jid"] ||= SecureRandom.hex(12)
29
- @transaction_backend.call { @redis_client.push(item) }
23
+ AfterCommitEverywhere.after_commit { @redis_client.push(item) }
30
24
  item["jid"]
31
25
  end
32
26
 
@@ -44,12 +38,10 @@ end
44
38
  # Use `Sidekiq.transactional_push!` in your sidekiq.rb initializer
45
39
  module Sidekiq
46
40
  def self.transactional_push!
47
- if ActiveRecord.version < Gem::Version.new("7.2")
48
- begin
49
- require "after_commit_everywhere"
50
- rescue LoadError
51
- raise %q(You need ActiveRecord >= 7.2 or to add `gem "after_commit_everywhere"` to your Gemfile to use Sidekiq's transactional client)
52
- end
41
+ begin
42
+ require "after_commit_everywhere"
43
+ rescue LoadError
44
+ raise %q(You need to add `gem "after_commit_everywhere"` to your Gemfile to use Sidekiq's transactional client)
53
45
  end
54
46
 
55
47
  Sidekiq.default_job_options["client_class"] = Sidekiq::TransactionAwareClient
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.3.10"
5
- MAJOR = 7
4
+ VERSION = "8.0.0.beta1"
5
+ MAJOR = 8
6
6
 
7
7
  def self.gem_version
8
8
  Gem::Version.new(VERSION)