sidekiq 6.5.10 → 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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +568 -18
  3. data/README.md +66 -38
  4. data/bin/kiq +17 -0
  5. data/bin/lint-herb +13 -0
  6. data/bin/multi_queue_bench +271 -0
  7. data/bin/sidekiq +3 -8
  8. data/bin/sidekiqload +215 -118
  9. data/bin/sidekiqmon +3 -0
  10. data/bin/webload +69 -0
  11. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  12. data/lib/generators/sidekiq/job_generator.rb +17 -3
  13. data/lib/generators/sidekiq/templates/job.rb.erb +1 -1
  14. data/lib/sidekiq/api.rb +526 -218
  15. data/lib/sidekiq/capsule.rb +135 -0
  16. data/lib/sidekiq/cli.rb +88 -96
  17. data/lib/sidekiq/client.rb +114 -47
  18. data/lib/sidekiq/component.rb +71 -4
  19. data/lib/sidekiq/config.rb +316 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +64 -0
  22. data/lib/sidekiq/fetch.rb +12 -14
  23. data/lib/sidekiq/iterable_job.rb +56 -0
  24. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  25. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  26. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  27. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  28. data/lib/sidekiq/job/iterable.rb +322 -0
  29. data/lib/sidekiq/job.rb +397 -10
  30. data/lib/sidekiq/job_logger.rb +14 -11
  31. data/lib/sidekiq/job_retry.rb +78 -28
  32. data/lib/sidekiq/job_util.rb +58 -16
  33. data/lib/sidekiq/launcher.rb +86 -70
  34. data/lib/sidekiq/loader.rb +57 -0
  35. data/lib/sidekiq/logger.rb +25 -94
  36. data/lib/sidekiq/manager.rb +11 -13
  37. data/lib/sidekiq/metrics/query.rb +84 -48
  38. data/lib/sidekiq/metrics/shared.rb +29 -14
  39. data/lib/sidekiq/metrics/tracking.rb +49 -30
  40. data/lib/sidekiq/middleware/chain.rb +19 -18
  41. data/lib/sidekiq/middleware/current_attributes.rb +78 -21
  42. data/lib/sidekiq/middleware/i18n.rb +2 -0
  43. data/lib/sidekiq/middleware/modules.rb +2 -0
  44. data/lib/sidekiq/monitor.rb +15 -5
  45. data/lib/sidekiq/paginator.rb +26 -4
  46. data/lib/sidekiq/processor.rb +78 -65
  47. data/lib/sidekiq/profiler.rb +73 -0
  48. data/lib/sidekiq/rails.rb +45 -60
  49. data/lib/sidekiq/redis_client_adapter.rb +35 -72
  50. data/lib/sidekiq/redis_connection.rb +54 -114
  51. data/lib/sidekiq/ring_buffer.rb +3 -0
  52. data/lib/sidekiq/scheduled.rb +28 -30
  53. data/lib/sidekiq/systemd.rb +2 -0
  54. data/lib/sidekiq/test_api.rb +331 -0
  55. data/lib/sidekiq/testing/inline.rb +2 -30
  56. data/lib/sidekiq/testing.rb +2 -342
  57. data/lib/sidekiq/transaction_aware_client.rb +23 -9
  58. data/lib/sidekiq/tui/controls.rb +53 -0
  59. data/lib/sidekiq/tui/filtering.rb +53 -0
  60. data/lib/sidekiq/tui/tabs/base_tab.rb +230 -0
  61. data/lib/sidekiq/tui/tabs/busy.rb +104 -0
  62. data/lib/sidekiq/tui/tabs/dead.rb +14 -0
  63. data/lib/sidekiq/tui/tabs/home.rb +115 -0
  64. data/lib/sidekiq/tui/tabs/metrics.rb +114 -0
  65. data/lib/sidekiq/tui/tabs/queues.rb +83 -0
  66. data/lib/sidekiq/tui/tabs/retries.rb +14 -0
  67. data/lib/sidekiq/tui/tabs/scheduled.rb +14 -0
  68. data/lib/sidekiq/tui/tabs/set_tab.rb +91 -0
  69. data/lib/sidekiq/tui/tabs.rb +15 -0
  70. data/lib/sidekiq/tui.rb +390 -0
  71. data/lib/sidekiq/version.rb +6 -1
  72. data/lib/sidekiq/web/action.rb +149 -64
  73. data/lib/sidekiq/web/application.rb +379 -281
  74. data/lib/sidekiq/web/config.rb +117 -0
  75. data/lib/sidekiq/web/helpers.rb +190 -71
  76. data/lib/sidekiq/web/router.rb +62 -75
  77. data/lib/sidekiq/web.rb +68 -101
  78. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  79. data/lib/sidekiq.rb +85 -269
  80. data/sidekiq.gemspec +15 -11
  81. data/web/assets/images/logo.png +0 -0
  82. data/web/assets/images/status.png +0 -0
  83. data/web/assets/javascripts/application.js +84 -24
  84. data/web/assets/javascripts/base-charts.js +120 -0
  85. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  86. data/web/assets/javascripts/dashboard-charts.js +194 -0
  87. data/web/assets/javascripts/dashboard.js +18 -234
  88. data/web/assets/javascripts/metrics.js +138 -120
  89. data/web/assets/stylesheets/style.css +776 -0
  90. data/web/locales/ar.yml +72 -70
  91. data/web/locales/cs.yml +64 -62
  92. data/web/locales/da.yml +62 -53
  93. data/web/locales/de.yml +67 -65
  94. data/web/locales/el.yml +4 -7
  95. data/web/locales/en.yml +89 -71
  96. data/web/locales/es.yml +91 -68
  97. data/web/locales/fa.yml +67 -65
  98. data/web/locales/fr.yml +82 -67
  99. data/web/locales/gd.yml +110 -0
  100. data/web/locales/he.yml +67 -64
  101. data/web/locales/hi.yml +95 -59
  102. data/web/locales/it.yml +94 -54
  103. data/web/locales/ja.yml +69 -70
  104. data/web/locales/ko.yml +54 -52
  105. data/web/locales/lt.yml +68 -66
  106. data/web/locales/nb.yml +63 -61
  107. data/web/locales/nl.yml +54 -52
  108. data/web/locales/pl.yml +47 -45
  109. data/web/locales/{pt-br.yml → pt-BR.yml} +81 -70
  110. data/web/locales/pt.yml +53 -51
  111. data/web/locales/ru.yml +69 -66
  112. data/web/locales/sv.yml +55 -53
  113. data/web/locales/ta.yml +100 -64
  114. data/web/locales/tr.yml +102 -0
  115. data/web/locales/uk.yml +87 -61
  116. data/web/locales/ur.yml +66 -64
  117. data/web/locales/vi.yml +69 -67
  118. data/web/locales/zh-CN.yml +107 -0
  119. data/web/locales/{zh-tw.yml → zh-TW.yml} +13 -3
  120. data/web/views/_footer.html.erb +32 -0
  121. data/web/views/_job_info.html.erb +115 -0
  122. data/web/views/_metrics_period_select.html.erb +15 -0
  123. data/web/views/_nav.html.erb +45 -0
  124. data/web/views/_paging.html.erb +26 -0
  125. data/web/views/_poll_link.html.erb +4 -0
  126. data/web/views/_summary.html.erb +40 -0
  127. data/web/views/busy.html.erb +155 -0
  128. data/web/views/dashboard.html.erb +104 -0
  129. data/web/views/dead.html.erb +38 -0
  130. data/web/views/filtering.html.erb +6 -0
  131. data/web/views/layout.html.erb +26 -0
  132. data/web/views/metrics.html.erb +85 -0
  133. data/web/views/metrics_for_job.html.erb +58 -0
  134. data/web/views/morgue.html.erb +69 -0
  135. data/web/views/profiles.html.erb +43 -0
  136. data/web/views/queue.html.erb +57 -0
  137. data/web/views/queues.html.erb +46 -0
  138. data/web/views/retries.html.erb +77 -0
  139. data/web/views/retry.html.erb +39 -0
  140. data/web/views/scheduled.html.erb +64 -0
  141. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  142. metadata +113 -70
  143. data/lib/sidekiq/delay.rb +0 -43
  144. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  145. data/lib/sidekiq/extensions/active_record.rb +0 -43
  146. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  147. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  148. data/lib/sidekiq/metrics/deploy.rb +0 -47
  149. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  150. data/lib/sidekiq/worker.rb +0 -370
  151. data/web/assets/javascripts/graph.js +0 -16
  152. data/web/assets/stylesheets/application-dark.css +0 -143
  153. data/web/assets/stylesheets/application-rtl.css +0 -242
  154. data/web/assets/stylesheets/application.css +0 -999
  155. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  156. data/web/assets/stylesheets/bootstrap.css +0 -5
  157. data/web/locales/zh-cn.yml +0 -93
  158. data/web/views/_footer.erb +0 -20
  159. data/web/views/_job_info.erb +0 -89
  160. data/web/views/_nav.erb +0 -52
  161. data/web/views/_paging.erb +0 -23
  162. data/web/views/_poll_link.erb +0 -4
  163. data/web/views/_status.erb +0 -4
  164. data/web/views/_summary.erb +0 -40
  165. data/web/views/busy.erb +0 -137
  166. data/web/views/dashboard.erb +0 -84
  167. data/web/views/dead.erb +0 -34
  168. data/web/views/layout.erb +0 -42
  169. data/web/views/metrics.erb +0 -69
  170. data/web/views/metrics_for_job.erb +0 -87
  171. data/web/views/morgue.erb +0 -78
  172. data/web/views/queue.erb +0 -59
  173. data/web/views/queues.erb +0 -38
  174. data/web/views/retries.erb +0 -83
  175. data/web/views/retry.erb +0 -34
  176. data/web/views/scheduled.erb +0 -57
  177. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -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"
@@ -16,8 +17,6 @@ class Sidekiq::Monitor
16
17
  return
17
18
  end
18
19
  send(section)
19
- rescue => e
20
- abort "Couldn't get status: #{e}"
21
20
  end
22
21
 
23
22
  def all
@@ -49,10 +48,21 @@ class Sidekiq::Monitor
49
48
  def processes
50
49
  puts "---- Processes (#{process_set.size}) ----"
51
50
  process_set.each_with_index do |process, index|
51
+ # Keep compatibility with legacy versions since we don't want to break sidekiqmon during rolling upgrades or downgrades.
52
+ queues =
53
+ if process["capsules"] # 8.0.6+
54
+ process["capsules"].values.map { |x| x["weights"].keys.join(", ") }
55
+ elsif process["weights"]
56
+ process["weights"].sort_by { |queue| queue[0] }.map { |capsule| capsule.map { |name, weight| (weight > 0) ? "#{name}: #{weight}" : name }.join(", ") }
57
+ else
58
+ process["queues"].sort
59
+ end
60
+
52
61
  puts "#{process["identity"]} #{tags_for(process)}"
53
62
  puts " Started: #{Time.at(process["started_at"])} (#{time_ago(process["started_at"])})"
54
63
  puts " Threads: #{process["concurrency"]} (#{process["busy"]} busy)"
55
- puts " Queues: #{split_multiline(process["queues"].sort, pad: 11)}"
64
+ puts " Queues: #{split_multiline(queues, pad: 11)}"
65
+ puts " Version: #{process["version"] || "Unknown"}" if process["version"] != Sidekiq::VERSION
56
66
  puts "" unless (index + 1) == process_set.size
57
67
  end
58
68
  end
@@ -85,13 +95,13 @@ class Sidekiq::Monitor
85
95
  pad = opts[:pad] || 0
86
96
  max_length = opts[:max_length] || (80 - pad)
87
97
  out = []
88
- line = ""
98
+ line = +""
89
99
  values.each do |value|
90
100
  if (line.length + value.length) > max_length
91
101
  out << line
92
102
  line = " " * pad
93
103
  end
94
- line << value + ", "
104
+ line << value + "; "
95
105
  end
96
106
  out << line[0..-3]
97
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,15 @@ 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 = conn.type(key)
27
+ TYPE_CACHE[key] = type unless type == "none"
28
+ end
15
29
  rev = opts && opts[:reverse]
16
30
 
17
31
  case type
@@ -19,9 +33,9 @@ module Sidekiq
19
33
  total_size, items = conn.multi { |transaction|
20
34
  transaction.zcard(key)
21
35
  if rev
22
- transaction.zrevrange(key, starting, ending, withscores: true)
36
+ transaction.zrange(key, starting, ending, "REV", "withscores")
23
37
  else
24
- transaction.zrange(key, starting, ending, withscores: true)
38
+ transaction.zrange(key, starting, ending, "withscores")
25
39
  end
26
40
  }
27
41
  [current_page, total_size, items]
@@ -46,10 +60,18 @@ module Sidekiq
46
60
 
47
61
  def page_items(items, pageidx = 1, page_size = 25)
48
62
  current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
63
+ # A negative page_size makes Array#[] return nil instead of a slice, which
64
+ # would crash callers iterating the result (e.g. the Web UI Busy page).
65
+ page_size = 0 if page_size < 0
49
66
  pageidx = current_page - 1
50
67
  starting = pageidx * page_size
51
68
  items = items.to_a
52
- [current_page, items.size, items[starting, page_size]]
69
+ total_size = items.size
70
+ if starting > total_size
71
+ starting = 0
72
+ current_page = 1
73
+ end
74
+ [current_page, total_size, items[starting, page_size]]
53
75
  end
54
76
  end
55
77
  end
@@ -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
  ##
@@ -26,18 +27,19 @@ module Sidekiq
26
27
 
27
28
  attr_reader :thread
28
29
  attr_reader :job
30
+ attr_reader :capsule
29
31
 
30
- def initialize(options, &block)
32
+ def initialize(capsule, &block)
33
+ @config = capsule.config
34
+ @capsule = capsule
31
35
  @callback = block
32
36
  @down = false
33
37
  @done = false
34
38
  @job = nil
35
39
  @thread = nil
36
- @config = options
37
- @strategy = options[:fetch]
38
- @reloader = options[:reloader] || proc { |&block| block.call }
39
- @job_logger = (options[:job_logger] || Sidekiq::JobLogger).new
40
- @retrier = Sidekiq::JobRetry.new(options)
40
+ @reloader = Sidekiq.default_configuration[:reloader]
41
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(@config)
42
+ @retrier = Sidekiq::JobRetry.new(@capsule)
41
43
  end
42
44
 
43
45
  def terminate(wait = false)
@@ -58,13 +60,21 @@ module Sidekiq
58
60
  @thread.value if wait
59
61
  end
60
62
 
63
+ def stopping?
64
+ @done
65
+ end
66
+
61
67
  def start
62
- @thread ||= safe_thread("processor", &method(:run))
68
+ @thread ||= safe_thread("#{capsule.name}/processor", &method(:run))
63
69
  end
64
70
 
65
- private unless $TESTING
71
+ private
66
72
 
67
73
  def run
74
+ # By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
75
+ # instead of the global pool in +Sidekiq::Config#redis_pool+.
76
+ Thread.current[:sidekiq_capsule] = @capsule
77
+
68
78
  process_one until @done
69
79
  @callback.call(self)
70
80
  rescue Sidekiq::Shutdown
@@ -80,7 +90,7 @@ module Sidekiq
80
90
  end
81
91
 
82
92
  def get_one
83
- uow = @strategy.retrieve_work
93
+ uow = capsule.fetcher.retrieve_work
84
94
  if @down
85
95
  logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }
86
96
  @down = nil
@@ -104,13 +114,17 @@ module Sidekiq
104
114
  def handle_fetch_exception(ex)
105
115
  unless @down
106
116
  @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
107
- logger.error("Error fetching job: #{ex}")
108
117
  handle_exception(ex)
109
118
  end
110
119
  sleep(1)
111
120
  nil
112
121
  end
113
122
 
123
+ def profile(job, &block)
124
+ return yield unless job["profile"]
125
+ Sidekiq::Profiler.new(config).call(job, &block)
126
+ end
127
+
114
128
  def dispatch(job_hash, queue, jobstr)
115
129
  # since middleware can mutate the job hash
116
130
  # we need to clone it to report the original
@@ -124,16 +138,19 @@ module Sidekiq
124
138
  @retrier.global(jobstr, queue) do
125
139
  @job_logger.call(job_hash, queue) do
126
140
  stats(jobstr, queue) do
127
- # Rails 5 requires a Reloader to wrap code execution. In order to
128
- # constantize the worker and instantiate an instance, we have to call
129
- # the Reloader. It handles code loading, db connection management, etc.
130
- # Effectively this block denotes a "unit of work" to Rails.
131
- @reloader.call do
132
- klass = constantize(job_hash["class"])
133
- inst = klass.new
134
- inst.jid = job_hash["jid"]
135
- @retrier.local(inst, jobstr, queue) do
136
- yield inst
141
+ profile(job_hash) do
142
+ # Rails 5 requires a Reloader to wrap code execution. In order to
143
+ # constantize the worker and instantiate an instance, we have to call
144
+ # the Reloader. It handles code loading, db connection management, etc.
145
+ # Effectively this block denotes a "unit of work" to Rails.
146
+ @reloader.call do
147
+ klass = Object.const_get(job_hash["class"])
148
+ instance = klass.new
149
+ instance.jid = job_hash["jid"]
150
+ instance._context = self
151
+ @retrier.local(instance, jobstr, queue) do
152
+ yield instance
153
+ end
137
154
  end
138
155
  end
139
156
  end
@@ -142,6 +159,11 @@ module Sidekiq
142
159
  end
143
160
  end
144
161
 
162
+ IGNORE_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :never}
163
+ private_constant :IGNORE_SHUTDOWN_INTERRUPTS
164
+ ALLOW_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :immediate}
165
+ private_constant :ALLOW_SHUTDOWN_INTERRUPTS
166
+
145
167
  def process(uow)
146
168
  jobstr = uow.job
147
169
  queue = uow.queue_name
@@ -151,55 +173,59 @@ module Sidekiq
151
173
  begin
152
174
  job_hash = Sidekiq.load_json(jobstr)
153
175
  rescue => ex
154
- handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
155
176
  now = Time.now.to_f
156
- config.redis do |conn|
177
+ redis do |conn|
157
178
  conn.multi do |xa|
158
179
  xa.zadd("dead", now.to_s, jobstr)
159
- xa.zremrangebyscore("dead", "-inf", now - config[:dead_timeout_in_seconds])
160
- xa.zremrangebyrank("dead", 0, - config[:dead_max_jobs])
180
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
181
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
161
182
  end
162
183
  end
184
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
163
185
  return uow.acknowledge
164
186
  end
165
187
 
166
188
  ack = false
167
- begin
168
- dispatch(job_hash, queue, jobstr) do |inst|
169
- @config.server_middleware.invoke(inst, job_hash, queue) do
170
- execute_job(inst, job_hash["args"])
189
+ Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
190
+ Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
191
+ dispatch(job_hash, queue, jobstr) do |instance|
192
+ config.server_middleware.invoke(instance, job_hash, queue) do
193
+ execute_job(instance, job_hash["args"])
194
+ end
171
195
  end
196
+ ack = true
197
+ rescue Sidekiq::Shutdown
198
+ # Had to force kill this job because it didn't finish
199
+ # within the timeout. Don't acknowledge the work since
200
+ # we didn't properly finish it.
201
+ rescue Sidekiq::JobRetry::Skip => s
202
+ # Skip means we handled this error elsewhere. We don't
203
+ # need to log or report the error.
204
+ ack = true
205
+ raise s
206
+ rescue Sidekiq::JobRetry::Handled => h
207
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
208
+ # signals that we created a retry successfully. We can acknowledge the job.
209
+ ack = true
210
+ e = h.cause || h
211
+ handle_exception(e, {context: "Job raised exception", job: job_hash})
212
+ raise e
213
+ rescue Exception => ex
214
+ # Unexpected error! This is very bad and indicates an exception that got past
215
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
216
+ # so it can be rescued when using Sidekiq Pro.
217
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
218
+ raise ex
172
219
  end
173
- ack = true
174
- rescue Sidekiq::Shutdown
175
- # Had to force kill this job because it didn't finish
176
- # within the timeout. Don't acknowledge the work since
177
- # we didn't properly finish it.
178
- rescue Sidekiq::JobRetry::Handled => h
179
- # this is the common case: job raised error and Sidekiq::JobRetry::Handled
180
- # signals that we created a retry successfully. We can acknowlege the job.
181
- ack = true
182
- e = h.cause || h
183
- handle_exception(e, {context: "Job raised exception", job: job_hash})
184
- raise e
185
- rescue Exception => ex
186
- # Unexpected error! This is very bad and indicates an exception that got past
187
- # the retry subsystem (e.g. network partition). We won't acknowledge the job
188
- # so it can be rescued when using Sidekiq Pro.
189
- handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
190
- raise ex
191
220
  ensure
192
221
  if ack
193
- # We don't want a shutdown signal to interrupt job acknowledgment.
194
- Thread.handle_interrupt(Sidekiq::Shutdown => :never) do
195
- uow.acknowledge
196
- end
222
+ uow.acknowledge
197
223
  end
198
224
  end
199
225
  end
200
226
 
201
- def execute_job(inst, cloned_args)
202
- inst.perform(*cloned_args)
227
+ def execute_job(instance, cloned_args)
228
+ instance.perform(*cloned_args)
203
229
  end
204
230
 
205
231
  # Ruby doesn't provide atomic counters out of the box so we'll
@@ -269,18 +295,5 @@ module Sidekiq
269
295
  PROCESSED.incr
270
296
  end
271
297
  end
272
-
273
- def constantize(str)
274
- return Object.const_get(str) unless str.include?("::")
275
-
276
- names = str.split("::")
277
- names.shift if names.empty? || names.first.empty?
278
-
279
- names.inject(Object) do |constant, name|
280
- # the false flag limits search for name to under the constant namespace
281
- # which mimics Rails' behaviour
282
- constant.const_get(name, false)
283
- end
284
- end
285
298
  end
286
299
  end
@@ -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["wrapped"] || 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
data/lib/sidekiq/rails.rb CHANGED
@@ -1,78 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/job"
4
-
5
3
  module Sidekiq
6
- class Rails < ::Rails::Engine
7
- class Reloader
8
- def initialize(app = ::Rails.application)
9
- @app = app
10
- end
4
+ begin
5
+ gem "railties", ">= 7.0"
6
+ require "rails"
7
+ require "sidekiq/job"
8
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
9
+
10
+ class Rails < ::Rails::Engine
11
+ class Reloader
12
+ def initialize(app = ::Rails.application)
13
+ @app = app
14
+ end
11
15
 
12
- def call
13
- @app.reloader.wrap do
14
- yield
16
+ def call
17
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
18
+ @app.reloader.wrap(**params) do
19
+ yield
20
+ end
15
21
  end
16
- end
17
22
 
18
- def inspect
19
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
20
- end
21
- end
23
+ def inspect
24
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
25
+ end
22
26
 
23
- # By including the Options module, we allow AJs to directly control sidekiq features
24
- # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
25
- # AJ retries don't show up in the Sidekiq UI Retries tab, save any error data, can't be
26
- # manually retried, don't automatically die, etc.
27
- #
28
- # class SomeJob < ActiveJob::Base
29
- # queue_as :default
30
- # sidekiq_options retry: 3, backtrace: 10
31
- # def perform
32
- # end
33
- # end
34
- initializer "sidekiq.active_job_integration" do
35
- ActiveSupport.on_load(:active_job) do
36
- include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
27
+ def to_hash
28
+ {app: @app.class.name}
29
+ end
37
30
  end
38
- end
39
31
 
40
- initializer "sidekiq.rails_logger" do
41
- Sidekiq.configure_server do |config|
42
- # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
43
- # it will appear in the Sidekiq console with all of the job context. See #5021 and
44
- # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
45
- unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
46
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
32
+ initializer "sidekiq.backtrace_cleaner" do
33
+ Sidekiq.configure_server do |config|
34
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
47
35
  end
48
36
  end
49
- end
50
-
51
- config.before_configuration do
52
- dep = ActiveSupport::Deprecation.new("7.0", "Sidekiq")
53
- dep.deprecate_methods(Sidekiq.singleton_class,
54
- default_worker_options: :default_job_options,
55
- "default_worker_options=": :default_job_options=)
56
- end
57
-
58
- # This hook happens after all initializers are run, just before returning
59
- # from config/environment.rb back to sidekiq/cli.rb.
60
- #
61
- # None of this matters on the client-side, only within the Sidekiq process itself.
62
- config.after_initialize do
63
- Sidekiq.configure_server do |config|
64
- config[:reloader] = Sidekiq::Rails::Reloader.new
65
37
 
66
- # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
67
- # it will appear in the Sidekiq console with all of the job context.
68
- unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
69
- if ::Rails::VERSION::STRING < "7.1"
70
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
71
- else
72
- ::Rails.logger = ::ActiveSupport::BroadcastLogger.new(::Rails.logger, config.logger)
38
+ # This hook happens after all initializers are run, just before returning
39
+ # from config/environment.rb back to sidekiq/cli.rb.
40
+ #
41
+ # None of this matters on the client-side, only within the Sidekiq process itself.
42
+ config.after_initialize do
43
+ Sidekiq.configure_server do |config|
44
+ config[:reloader] = Sidekiq::Rails::Reloader.new
45
+
46
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
47
+ # it will appear in the Sidekiq console with all of the job context.
48
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
49
+ if ::Rails.logger.respond_to?(:broadcast_to)
50
+ ::Rails.logger.broadcast_to(config.logger)
51
+ elsif ::ActiveSupport::Logger.respond_to?(:broadcast)
52
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
53
+ else
54
+ ::Rails.logger = ::ActiveSupport::BroadcastLogger.new(::Rails.logger, config.logger)
55
+ end
73
56
  end
74
57
  end
75
58
  end
76
59
  end
60
+ rescue Gem::LoadError
61
+ # Rails not available or version requirement not met
77
62
  end
78
63
  end