sidekiq 7.1.4 → 8.0.9

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +333 -0
  3. data/README.md +16 -13
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +31 -22
  6. data/bin/webload +69 -0
  7. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +121 -0
  8. data/lib/generators/sidekiq/job_generator.rb +2 -0
  9. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  10. data/lib/sidekiq/api.rb +260 -67
  11. data/lib/sidekiq/capsule.rb +17 -8
  12. data/lib/sidekiq/cli.rb +19 -20
  13. data/lib/sidekiq/client.rb +48 -15
  14. data/lib/sidekiq/component.rb +64 -3
  15. data/lib/sidekiq/config.rb +60 -18
  16. data/lib/sidekiq/deploy.rb +4 -2
  17. data/lib/sidekiq/embedded.rb +4 -1
  18. data/lib/sidekiq/fetch.rb +2 -1
  19. data/lib/sidekiq/iterable_job.rb +56 -0
  20. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  21. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  22. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  23. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  24. data/lib/sidekiq/job/iterable.rb +322 -0
  25. data/lib/sidekiq/job.rb +16 -5
  26. data/lib/sidekiq/job_logger.rb +15 -12
  27. data/lib/sidekiq/job_retry.rb +41 -13
  28. data/lib/sidekiq/job_util.rb +7 -1
  29. data/lib/sidekiq/launcher.rb +23 -11
  30. data/lib/sidekiq/loader.rb +57 -0
  31. data/lib/sidekiq/logger.rb +25 -69
  32. data/lib/sidekiq/manager.rb +0 -1
  33. data/lib/sidekiq/metrics/query.rb +76 -45
  34. data/lib/sidekiq/metrics/shared.rb +23 -9
  35. data/lib/sidekiq/metrics/tracking.rb +32 -15
  36. data/lib/sidekiq/middleware/current_attributes.rb +39 -14
  37. data/lib/sidekiq/middleware/i18n.rb +2 -0
  38. data/lib/sidekiq/middleware/modules.rb +2 -0
  39. data/lib/sidekiq/monitor.rb +6 -9
  40. data/lib/sidekiq/paginator.rb +16 -3
  41. data/lib/sidekiq/processor.rb +37 -20
  42. data/lib/sidekiq/profiler.rb +73 -0
  43. data/lib/sidekiq/rails.rb +47 -57
  44. data/lib/sidekiq/redis_client_adapter.rb +25 -8
  45. data/lib/sidekiq/redis_connection.rb +49 -9
  46. data/lib/sidekiq/ring_buffer.rb +3 -0
  47. data/lib/sidekiq/scheduled.rb +2 -2
  48. data/lib/sidekiq/systemd.rb +2 -0
  49. data/lib/sidekiq/testing.rb +34 -15
  50. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  51. data/lib/sidekiq/version.rb +6 -2
  52. data/lib/sidekiq/web/action.rb +149 -64
  53. data/lib/sidekiq/web/application.rb +367 -297
  54. data/lib/sidekiq/web/config.rb +120 -0
  55. data/lib/sidekiq/web/csrf_protection.rb +8 -5
  56. data/lib/sidekiq/web/helpers.rb +146 -64
  57. data/lib/sidekiq/web/router.rb +61 -74
  58. data/lib/sidekiq/web.rb +53 -106
  59. data/lib/sidekiq.rb +11 -4
  60. data/sidekiq.gemspec +6 -5
  61. data/web/assets/images/logo.png +0 -0
  62. data/web/assets/images/status.png +0 -0
  63. data/web/assets/javascripts/application.js +66 -24
  64. data/web/assets/javascripts/base-charts.js +30 -16
  65. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  66. data/web/assets/javascripts/dashboard-charts.js +37 -11
  67. data/web/assets/javascripts/dashboard.js +15 -11
  68. data/web/assets/javascripts/metrics.js +50 -34
  69. data/web/assets/stylesheets/style.css +776 -0
  70. data/web/locales/ar.yml +2 -0
  71. data/web/locales/cs.yml +2 -0
  72. data/web/locales/da.yml +2 -0
  73. data/web/locales/de.yml +2 -0
  74. data/web/locales/el.yml +2 -0
  75. data/web/locales/en.yml +12 -1
  76. data/web/locales/es.yml +25 -2
  77. data/web/locales/fa.yml +2 -0
  78. data/web/locales/fr.yml +2 -1
  79. data/web/locales/gd.yml +2 -1
  80. data/web/locales/he.yml +2 -0
  81. data/web/locales/hi.yml +2 -0
  82. data/web/locales/it.yml +41 -1
  83. data/web/locales/ja.yml +2 -1
  84. data/web/locales/ko.yml +2 -0
  85. data/web/locales/lt.yml +2 -0
  86. data/web/locales/nb.yml +2 -0
  87. data/web/locales/nl.yml +2 -0
  88. data/web/locales/pl.yml +2 -0
  89. data/web/locales/{pt-br.yml → pt-BR.yml} +4 -3
  90. data/web/locales/pt.yml +2 -0
  91. data/web/locales/ru.yml +2 -0
  92. data/web/locales/sv.yml +2 -0
  93. data/web/locales/ta.yml +2 -0
  94. data/web/locales/tr.yml +102 -0
  95. data/web/locales/uk.yml +29 -4
  96. data/web/locales/ur.yml +2 -0
  97. data/web/locales/vi.yml +2 -0
  98. data/web/locales/{zh-cn.yml → zh-CN.yml} +86 -74
  99. data/web/locales/{zh-tw.yml → zh-TW.yml} +3 -2
  100. data/web/views/_footer.erb +31 -22
  101. data/web/views/_job_info.erb +91 -89
  102. data/web/views/_metrics_period_select.erb +13 -10
  103. data/web/views/_nav.erb +14 -21
  104. data/web/views/_paging.erb +22 -21
  105. data/web/views/_poll_link.erb +2 -2
  106. data/web/views/_summary.erb +23 -23
  107. data/web/views/busy.erb +123 -125
  108. data/web/views/dashboard.erb +71 -82
  109. data/web/views/dead.erb +31 -27
  110. data/web/views/filtering.erb +6 -0
  111. data/web/views/layout.erb +13 -29
  112. data/web/views/metrics.erb +70 -68
  113. data/web/views/metrics_for_job.erb +30 -40
  114. data/web/views/morgue.erb +65 -70
  115. data/web/views/profiles.erb +43 -0
  116. data/web/views/queue.erb +54 -52
  117. data/web/views/queues.erb +43 -37
  118. data/web/views/retries.erb +70 -75
  119. data/web/views/retry.erb +32 -27
  120. data/web/views/scheduled.erb +63 -55
  121. data/web/views/scheduled_job_info.erb +3 -3
  122. metadata +49 -27
  123. data/web/assets/stylesheets/application-dark.css +0 -147
  124. data/web/assets/stylesheets/application-rtl.css +0 -153
  125. data/web/assets/stylesheets/application.css +0 -724
  126. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  127. data/web/assets/stylesheets/bootstrap.css +0 -5
  128. data/web/views/_status.erb +0 -4
@@ -1,10 +1,21 @@
1
- require "concurrent"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
4
  module Metrics
5
- # This is the only dependency on concurrent-ruby in Sidekiq but it's
6
- # mandatory for thread-safety until MRI supports atomic operations on values.
7
- Counter = ::Concurrent::AtomicFixnum
5
+ class Counter
6
+ def initialize
7
+ @value = 0
8
+ @lock = Mutex.new
9
+ end
10
+
11
+ def increment
12
+ @lock.synchronize { @value += 1 }
13
+ end
14
+
15
+ def value
16
+ @lock.synchronize { @value }
17
+ end
18
+ end
8
19
 
9
20
  # Implements space-efficient but statistically useful histogram storage.
10
21
  # A precise time histogram stores every time. Instead we break times into a set of
@@ -14,7 +25,10 @@ module Sidekiq
14
25
  #
15
26
  # To store this data, we use Redis' BITFIELD command to store unsigned 16-bit counters
16
27
  # per bucket per klass per minute. It's unlikely that most people will be executing more
17
- # than 1000 job/sec for a full minute of a specific type.
28
+ # than 1000 job/sec for a full minute of a specific type (i.e. overflow 65,536).
29
+ #
30
+ # Histograms are only stored at the fine-grained level, they are not rolled up
31
+ # for longer-term buckets.
18
32
  class Histogram
19
33
  include Enumerable
20
34
 
@@ -71,15 +85,15 @@ module Sidekiq
71
85
  end
72
86
 
73
87
  def fetch(conn, now = Time.now)
74
- window = now.utc.strftime("%d-%H:%-M")
75
- key = "#{@klass}-#{window}"
88
+ window = now.utc.strftime("%-d-%-H:%-M")
89
+ key = "h|#{@klass}-#{window}"
76
90
  conn.bitfield_ro(key, *FETCH)
77
91
  end
78
92
 
79
93
  def persist(conn, now = Time.now)
80
94
  buckets, @buckets = @buckets, []
81
- window = now.utc.strftime("%d-%H:%-M")
82
- key = "#{@klass}-#{window}"
95
+ window = now.utc.strftime("%-d-%-H:%-M")
96
+ key = "h|#{@klass}-#{window}"
83
97
  cmd = [key, "OVERFLOW", "SAT"]
84
98
  buckets.each_with_index do |counter, idx|
85
99
  val = counter.value
@@ -19,23 +19,23 @@ module Sidekiq
19
19
  end
20
20
 
21
21
  def track(queue, klass)
22
- start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
22
+ start = mono_ms
23
23
  time_ms = 0
24
24
  begin
25
25
  begin
26
26
  yield
27
27
  ensure
28
- finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
28
+ finish = mono_ms
29
29
  time_ms = finish - start
30
30
  end
31
31
  # We don't track time for failed jobs as they can have very unpredictable
32
32
  # execution times. more important to know average time for successful jobs so we
33
33
  # can better recognize when a perf regression is introduced.
34
- @lock.synchronize {
35
- @grams[klass].record_time(time_ms)
36
- @jobs["#{klass}|ms"] += time_ms
37
- @totals["ms"] += time_ms
38
- }
34
+ track_time(klass, time_ms)
35
+ rescue JobRetry::Skip
36
+ # This is raised when iterable job is interrupted.
37
+ track_time(klass, time_ms)
38
+ raise
39
39
  rescue Exception
40
40
  @lock.synchronize {
41
41
  @jobs["#{klass}|f"] += 1
@@ -51,7 +51,7 @@ module Sidekiq
51
51
  end
52
52
 
53
53
  # LONG_TERM = 90 * 24 * 60 * 60
54
- # MID_TERM = 7 * 24 * 60 * 60
54
+ MID_TERM = 3 * 24 * 60 * 60
55
55
  SHORT_TERM = 8 * 60 * 60
56
56
 
57
57
  def flush(time = Time.now)
@@ -62,8 +62,10 @@ module Sidekiq
62
62
 
63
63
  now = time.utc
64
64
  # nowdate = now.strftime("%Y%m%d")
65
- # nowhour = now.strftime("%Y%m%d|%-H")
66
- nowmin = now.strftime("%Y%m%d|%-H:%-M")
65
+ # "250214|8:4" is the 10 minute bucket for Feb 14 2025, 08:43
66
+ nowmid = now.strftime("%y%m%d|%-H:%M")[0..-2]
67
+ # "250214|8:43" is the 1 minute bucket for Feb 14 2025, 08:43
68
+ nowshort = now.strftime("%y%m%d|%-H:%M")
67
69
  count = 0
68
70
 
69
71
  redis do |conn|
@@ -81,8 +83,8 @@ module Sidekiq
81
83
  # daily or hourly rollups.
82
84
  [
83
85
  # ["j", jobs, nowdate, LONG_TERM],
84
- # ["j", jobs, nowhour, MID_TERM],
85
- ["j", jobs, nowmin, SHORT_TERM]
86
+ ["j", jobs, nowmid, MID_TERM],
87
+ ["j", jobs, nowshort, SHORT_TERM]
86
88
  ].each do |prefix, data, bucket, ttl|
87
89
  conn.pipelined do |xa|
88
90
  stats = "#{prefix}|#{bucket}"
@@ -100,15 +102,27 @@ module Sidekiq
100
102
 
101
103
  private
102
104
 
105
+ def track_time(klass, time_ms)
106
+ @lock.synchronize {
107
+ @grams[klass].record_time(time_ms)
108
+ @jobs["#{klass}|ms"] += time_ms
109
+ @totals["ms"] += time_ms
110
+ }
111
+ end
112
+
103
113
  def reset
104
114
  @lock.synchronize {
105
115
  array = [@totals, @jobs, @grams]
106
- @totals = Hash.new(0)
107
- @jobs = Hash.new(0)
108
- @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
116
+ reset_instance_variables
109
117
  array
110
118
  }
111
119
  end
120
+
121
+ def reset_instance_variables
122
+ @totals = Hash.new(0)
123
+ @jobs = Hash.new(0)
124
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
125
+ end
112
126
  end
113
127
 
114
128
  class Middleware
@@ -133,4 +147,7 @@ Sidekiq.configure_server do |config|
133
147
  config.on(:beat) do
134
148
  exec.flush
135
149
  end
150
+ config.on(:exit) do
151
+ exec.flush
152
+ end
136
153
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job/arguments"
1
4
  require "active_support/current_attributes"
2
5
 
3
6
  module Sidekiq
@@ -18,6 +21,8 @@ module Sidekiq
18
21
  # Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
19
22
  #
20
23
  module CurrentAttributes
24
+ Serializer = ::ActiveJob::Arguments
25
+
21
26
  class Save
22
27
  include Sidekiq::ClientMiddleware
23
28
 
@@ -31,7 +36,7 @@ module Sidekiq
31
36
  attrs = strklass.constantize.attributes
32
37
  # Retries can push the job N times, we don't
33
38
  # want retries to reset cattr. #5692, #5090
34
- job[key] = attrs if attrs.any?
39
+ job[key] = Serializer.serialize(attrs) if attrs.any?
35
40
  end
36
41
  end
37
42
  yield
@@ -45,23 +50,42 @@ module Sidekiq
45
50
  @cattrs = cattrs
46
51
  end
47
52
 
48
- def call(_, job, _, &block)
49
- cattrs_to_reset = []
53
+ def call(_, job, *, &block)
54
+ klass_attrs = {}
50
55
 
51
56
  @cattrs.each do |(key, strklass)|
52
- if job.has_key?(key)
53
- constklass = strklass.constantize
54
- cattrs_to_reset << constklass
57
+ next unless job.has_key?(key)
55
58
 
56
- job[key].each do |(attribute, value)|
57
- constklass.public_send("#{attribute}=", value)
58
- end
59
- end
59
+ klass_attrs[strklass.constantize] = Serializer.deserialize(job[key]).to_h
60
60
  end
61
61
 
62
- yield
63
- ensure
64
- cattrs_to_reset.each(&:reset)
62
+ wrap(klass_attrs.to_a, &block)
63
+ end
64
+
65
+ private
66
+
67
+ def wrap(klass_attrs, &block)
68
+ klass, attrs = klass_attrs.shift
69
+ return block.call unless klass
70
+
71
+ retried = false
72
+
73
+ begin
74
+ set_succeeded = false
75
+ klass.set(attrs) do
76
+ set_succeeded = true
77
+ wrap(klass_attrs, &block)
78
+ end
79
+ rescue NoMethodError
80
+ # Don't retry if the no method error didn't come from current attributes
81
+ raise if retried || set_succeeded
82
+
83
+ # It is possible that the `CurrentAttributes` definition
84
+ # was changed before the job started processing.
85
+ attrs = attrs.select { |attr| klass.respond_to?(attr) }
86
+ retried = true
87
+ retry
88
+ end
65
89
  end
66
90
  end
67
91
 
@@ -69,8 +93,9 @@ module Sidekiq
69
93
  def persist(klass_or_array, config = Sidekiq.default_configuration)
70
94
  cattrs = build_cattrs_hash(klass_or_array)
71
95
 
96
+ config.client_middleware.prepend Load, cattrs
72
97
  config.client_middleware.add Save, cattrs
73
- config.server_middleware.add Load, cattrs
98
+ config.server_middleware.prepend Load, cattrs
74
99
  end
75
100
 
76
101
  private
@@ -11,6 +11,7 @@ module Sidekiq::Middleware::I18n
11
11
  # to be sent to Sidekiq.
12
12
  class Client
13
13
  include Sidekiq::ClientMiddleware
14
+
14
15
  def call(_jobclass, job, _queue, _redis)
15
16
  job["locale"] ||= I18n.locale
16
17
  yield
@@ -20,6 +21,7 @@ module Sidekiq::Middleware::I18n
20
21
  # Pull the msg locale out and set the current thread to use it.
21
22
  class Server
22
23
  include Sidekiq::ServerMiddleware
24
+
23
25
  def call(_jobclass, job, _queue, &block)
24
26
  I18n.with_locale(job.fetch("locale", I18n.default_locale), &block)
25
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  # Server-side middleware must import this Module in order
3
5
  # to get access to server resources during `call`.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "fileutils"
4
5
  require "sidekiq/api"
@@ -48,14 +49,10 @@ class Sidekiq::Monitor
48
49
  puts "---- Processes (#{process_set.size}) ----"
49
50
  process_set.each_with_index do |process, index|
50
51
  # 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
52
  queues =
58
- if process["weights"]
53
+ if process["capsules"] # 8.0.6+
54
+ process["capsules"].values.map { |x| x["weights"].keys.join(", ") }
55
+ elsif process["weights"]
59
56
  process["weights"].sort_by { |queue| queue[0] }.map { |capsule| capsule.map { |name, weight| (weight > 0) ? "#{name}: #{weight}" : name }.join(", ") }
60
57
  else
61
58
  process["queues"].sort
@@ -98,13 +95,13 @@ class Sidekiq::Monitor
98
95
  pad = opts[:pad] || 0
99
96
  max_length = opts[:max_length] || (80 - pad)
100
97
  out = []
101
- line = ""
98
+ line = +""
102
99
  values.each do |value|
103
100
  if (line.length + value.length) > max_length
104
101
  out << line
105
102
  line = " " * pad
106
103
  end
107
- line << value + ", "
104
+ line << value + "; "
108
105
  end
109
106
  out << line[0..-3]
110
107
  out.join("\n")
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Paginator
5
+ TYPE_CACHE = {
6
+ "dead" => "zset",
7
+ "retry" => "zset",
8
+ "schedule" => "zset"
9
+ }
10
+
5
11
  def page(key, pageidx = 1, page_size = 25, opts = nil)
6
12
  current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
7
13
  pageidx = current_page - 1
@@ -11,7 +17,14 @@ module Sidekiq
11
17
  ending = starting + page_size - 1
12
18
 
13
19
  Sidekiq.redis do |conn|
14
- 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
15
28
  rev = opts && opts[:reverse]
16
29
 
17
30
  case type
@@ -19,9 +32,9 @@ module Sidekiq
19
32
  total_size, items = conn.multi { |transaction|
20
33
  transaction.zcard(key)
21
34
  if rev
22
- transaction.zrange(key, starting, ending, "REV", withscores: true)
35
+ transaction.zrange(key, starting, ending, "REV", "withscores")
23
36
  else
24
- transaction.zrange(key, starting, ending, withscores: true)
37
+ transaction.zrange(key, starting, ending, "withscores")
25
38
  end
26
39
  }
27
40
  [current_page, total_size, items]
@@ -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
  ##
@@ -36,7 +37,7 @@ module Sidekiq
36
37
  @job = nil
37
38
  @thread = nil
38
39
  @reloader = Sidekiq.default_configuration[:reloader]
39
- @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(logger)
40
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
40
41
  @retrier = Sidekiq::JobRetry.new(capsule)
41
42
  end
42
43
 
@@ -58,11 +59,15 @@ module Sidekiq
58
59
  @thread.value if wait
59
60
  end
60
61
 
62
+ def stopping?
63
+ @done
64
+ end
65
+
61
66
  def start
62
67
  @thread ||= safe_thread("#{config.name}/processor", &method(:run))
63
68
  end
64
69
 
65
- private unless $TESTING
70
+ private
66
71
 
67
72
  def run
68
73
  # By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
@@ -108,13 +113,17 @@ module Sidekiq
108
113
  def handle_fetch_exception(ex)
109
114
  unless @down
110
115
  @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
111
- logger.error("Error fetching job: #{ex}")
112
116
  handle_exception(ex)
113
117
  end
114
118
  sleep(1)
115
119
  nil
116
120
  end
117
121
 
122
+ def profile(job, &block)
123
+ return yield unless job["profile"]
124
+ Sidekiq::Profiler.new(config).call(job, &block)
125
+ end
126
+
118
127
  def dispatch(job_hash, queue, jobstr)
119
128
  # since middleware can mutate the job hash
120
129
  # we need to clone it to report the original
@@ -128,16 +137,19 @@ module Sidekiq
128
137
  @retrier.global(jobstr, queue) do
129
138
  @job_logger.call(job_hash, queue) do
130
139
  stats(jobstr, queue) do
131
- # Rails 5 requires a Reloader to wrap code execution. In order to
132
- # constantize the worker and instantiate an instance, we have to call
133
- # the Reloader. It handles code loading, db connection management, etc.
134
- # Effectively this block denotes a "unit of work" to Rails.
135
- @reloader.call do
136
- klass = Object.const_get(job_hash["class"])
137
- inst = klass.new
138
- inst.jid = job_hash["jid"]
139
- @retrier.local(inst, jobstr, queue) do
140
- yield inst
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
141
153
  end
142
154
  end
143
155
  end
@@ -160,7 +172,6 @@ module Sidekiq
160
172
  begin
161
173
  job_hash = Sidekiq.load_json(jobstr)
162
174
  rescue => ex
163
- handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
164
175
  now = Time.now.to_f
165
176
  redis do |conn|
166
177
  conn.multi do |xa|
@@ -169,15 +180,16 @@ module Sidekiq
169
180
  xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
170
181
  end
171
182
  end
183
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
172
184
  return uow.acknowledge
173
185
  end
174
186
 
175
187
  ack = false
176
188
  Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
177
189
  Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
178
- dispatch(job_hash, queue, jobstr) do |inst|
179
- config.server_middleware.invoke(inst, job_hash, queue) do
180
- execute_job(inst, job_hash["args"])
190
+ dispatch(job_hash, queue, jobstr) do |instance|
191
+ config.server_middleware.invoke(instance, job_hash, queue) do
192
+ execute_job(instance, job_hash["args"])
181
193
  end
182
194
  end
183
195
  ack = true
@@ -185,9 +197,14 @@ module Sidekiq
185
197
  # Had to force kill this job because it didn't finish
186
198
  # within the timeout. Don't acknowledge the work since
187
199
  # we didn't properly finish it.
200
+ rescue Sidekiq::JobRetry::Skip => s
201
+ # Skip means we handled this error elsewhere. We don't
202
+ # need to log or report the error.
203
+ ack = true
204
+ raise s
188
205
  rescue Sidekiq::JobRetry::Handled => h
189
206
  # this is the common case: job raised error and Sidekiq::JobRetry::Handled
190
- # signals that we created a retry successfully. We can acknowlege the job.
207
+ # signals that we created a retry successfully. We can acknowledge the job.
191
208
  ack = true
192
209
  e = h.cause || h
193
210
  handle_exception(e, {context: "Job raised exception", job: job_hash})
@@ -206,8 +223,8 @@ module Sidekiq
206
223
  end
207
224
  end
208
225
 
209
- def execute_job(inst, cloned_args)
210
- inst.perform(*cloned_args)
226
+ def execute_job(instance, cloned_args)
227
+ instance.perform(*cloned_args)
211
228
  end
212
229
 
213
230
  # Ruby doesn't provide atomic counters out of the box so we'll
@@ -0,0 +1,73 @@
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
+
15
+ def initialize(config)
16
+ @config = config
17
+ @vernier_output_dir = ENV.fetch("VERNIER_OUTPUT_DIR") { Dir.tmpdir }
18
+ end
19
+
20
+ def call(job, &block)
21
+ return yield unless job["profile"]
22
+
23
+ token = job["profile"]
24
+ type = job["class"]
25
+ jid = job["jid"]
26
+ started_at = Time.now
27
+
28
+ rundata = {
29
+ started_at: started_at.to_i,
30
+ token: token,
31
+ type: type,
32
+ jid: jid,
33
+ # .gz extension tells Vernier to compress the data
34
+ filename: File.join(
35
+ @vernier_output_dir,
36
+ "#{token}-#{type}-#{jid}-#{started_at.strftime("%Y%m%d-%H%M%S")}.json.gz"
37
+ )
38
+ }
39
+ profiler_options = profiler_options(job, rundata)
40
+
41
+ require "vernier"
42
+ begin
43
+ a = Time.now
44
+ rc = Vernier.profile(**profiler_options, &block)
45
+ b = Time.now
46
+
47
+ # Failed jobs will raise an exception on previous line and skip this
48
+ # block. Only successful jobs will persist profile data to Redis.
49
+ key = "#{token}-#{jid}"
50
+ data = File.read(rundata[:filename])
51
+ redis do |conn|
52
+ conn.multi do |m|
53
+ m.zadd("profiles", Time.now.to_f + EXPIRY, key)
54
+ m.hset(key, rundata.merge(elapsed: (b - a), data: data, size: data.bytesize))
55
+ m.expire(key, EXPIRY)
56
+ end
57
+ end
58
+ rc
59
+ ensure
60
+ FileUtils.rm_f(rundata[:filename])
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def profiler_options(job, rundata)
67
+ profiler_options = (job["profiler_options"] || {}).transform_keys(&:to_sym)
68
+ profiler_options[:mode] = profiler_options[:mode].to_sym if profiler_options[:mode]
69
+
70
+ DEFAULT_OPTIONS.merge(profiler_options, {out: rundata[:filename]})
71
+ end
72
+ end
73
+ end