sidekiq 6.2.2 → 8.1.5

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +726 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +70 -39
  5. data/bin/kiq +17 -0
  6. data/bin/lint-herb +13 -0
  7. data/bin/multi_queue_bench +271 -0
  8. data/bin/sidekiq +4 -9
  9. data/bin/sidekiqload +214 -115
  10. data/bin/sidekiqmon +4 -1
  11. data/bin/webload +69 -0
  12. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +124 -0
  13. data/lib/generators/sidekiq/job_generator.rb +71 -0
  14. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +3 -3
  15. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  16. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  17. data/lib/sidekiq/api.rb +729 -264
  18. data/lib/sidekiq/capsule.rb +135 -0
  19. data/lib/sidekiq/cli.rb +124 -100
  20. data/lib/sidekiq/client.rb +153 -106
  21. data/lib/sidekiq/component.rb +132 -0
  22. data/lib/sidekiq/config.rb +320 -0
  23. data/lib/sidekiq/deploy.rb +64 -0
  24. data/lib/sidekiq/embedded.rb +64 -0
  25. data/lib/sidekiq/fetch.rb +27 -26
  26. data/lib/sidekiq/iterable_job.rb +56 -0
  27. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  28. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  29. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  30. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  31. data/lib/sidekiq/job/iterable.rb +322 -0
  32. data/lib/sidekiq/job.rb +397 -5
  33. data/lib/sidekiq/job_logger.rb +23 -32
  34. data/lib/sidekiq/job_retry.rb +141 -68
  35. data/lib/sidekiq/job_util.rb +113 -0
  36. data/lib/sidekiq/launcher.rb +122 -98
  37. data/lib/sidekiq/loader.rb +57 -0
  38. data/lib/sidekiq/logger.rb +27 -106
  39. data/lib/sidekiq/manager.rb +41 -43
  40. data/lib/sidekiq/metrics/query.rb +184 -0
  41. data/lib/sidekiq/metrics/shared.rb +109 -0
  42. data/lib/sidekiq/metrics/tracking.rb +153 -0
  43. data/lib/sidekiq/middleware/chain.rb +96 -51
  44. data/lib/sidekiq/middleware/current_attributes.rb +120 -0
  45. data/lib/sidekiq/middleware/i18n.rb +8 -4
  46. data/lib/sidekiq/middleware/modules.rb +23 -0
  47. data/lib/sidekiq/monitor.rb +16 -6
  48. data/lib/sidekiq/paginator.rb +37 -10
  49. data/lib/sidekiq/processor.rb +105 -87
  50. data/lib/sidekiq/profiler.rb +73 -0
  51. data/lib/sidekiq/rails.rb +49 -36
  52. data/lib/sidekiq/redis_client_adapter.rb +117 -0
  53. data/lib/sidekiq/redis_connection.rb +55 -86
  54. data/lib/sidekiq/ring_buffer.rb +32 -0
  55. data/lib/sidekiq/scheduled.rb +106 -50
  56. data/lib/sidekiq/systemd.rb +2 -0
  57. data/lib/sidekiq/test_api.rb +331 -0
  58. data/lib/sidekiq/testing/inline.rb +2 -30
  59. data/lib/sidekiq/testing.rb +2 -342
  60. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  61. data/lib/sidekiq/tui/controls.rb +53 -0
  62. data/lib/sidekiq/tui/filtering.rb +53 -0
  63. data/lib/sidekiq/tui/tabs/base_tab.rb +204 -0
  64. data/lib/sidekiq/tui/tabs/busy.rb +118 -0
  65. data/lib/sidekiq/tui/tabs/dead.rb +19 -0
  66. data/lib/sidekiq/tui/tabs/home.rb +144 -0
  67. data/lib/sidekiq/tui/tabs/metrics.rb +131 -0
  68. data/lib/sidekiq/tui/tabs/queues.rb +95 -0
  69. data/lib/sidekiq/tui/tabs/retries.rb +19 -0
  70. data/lib/sidekiq/tui/tabs/scheduled.rb +19 -0
  71. data/lib/sidekiq/tui/tabs/set_tab.rb +96 -0
  72. data/lib/sidekiq/tui/tabs.rb +15 -0
  73. data/lib/sidekiq/tui.rb +382 -0
  74. data/lib/sidekiq/version.rb +6 -1
  75. data/lib/sidekiq/web/action.rb +149 -64
  76. data/lib/sidekiq/web/application.rb +376 -268
  77. data/lib/sidekiq/web/config.rb +117 -0
  78. data/lib/sidekiq/web/helpers.rb +213 -87
  79. data/lib/sidekiq/web/router.rb +61 -74
  80. data/lib/sidekiq/web.rb +71 -100
  81. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  82. data/lib/sidekiq.rb +95 -196
  83. data/sidekiq.gemspec +14 -11
  84. data/web/assets/images/logo.png +0 -0
  85. data/web/assets/images/status.png +0 -0
  86. data/web/assets/javascripts/application.js +171 -57
  87. data/web/assets/javascripts/base-charts.js +120 -0
  88. data/web/assets/javascripts/chart.min.js +13 -0
  89. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  90. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  91. data/web/assets/javascripts/dashboard-charts.js +194 -0
  92. data/web/assets/javascripts/dashboard.js +41 -274
  93. data/web/assets/javascripts/metrics.js +280 -0
  94. data/web/assets/stylesheets/style.css +776 -0
  95. data/web/locales/ar.yml +72 -70
  96. data/web/locales/cs.yml +64 -62
  97. data/web/locales/da.yml +62 -53
  98. data/web/locales/de.yml +67 -65
  99. data/web/locales/el.yml +45 -24
  100. data/web/locales/en.yml +93 -69
  101. data/web/locales/es.yml +91 -68
  102. data/web/locales/fa.yml +67 -65
  103. data/web/locales/fr.yml +82 -67
  104. data/web/locales/gd.yml +110 -0
  105. data/web/locales/he.yml +67 -64
  106. data/web/locales/hi.yml +61 -59
  107. data/web/locales/it.yml +94 -54
  108. data/web/locales/ja.yml +74 -68
  109. data/web/locales/ko.yml +54 -52
  110. data/web/locales/lt.yml +68 -66
  111. data/web/locales/nb.yml +63 -61
  112. data/web/locales/nl.yml +54 -52
  113. data/web/locales/pl.yml +47 -45
  114. data/web/locales/{pt-br.yml → pt-BR.yml} +85 -56
  115. data/web/locales/pt.yml +53 -51
  116. data/web/locales/ru.yml +69 -66
  117. data/web/locales/sv.yml +55 -53
  118. data/web/locales/ta.yml +62 -60
  119. data/web/locales/tr.yml +102 -0
  120. data/web/locales/uk.yml +87 -61
  121. data/web/locales/ur.yml +66 -64
  122. data/web/locales/vi.yml +69 -67
  123. data/web/locales/zh-CN.yml +107 -0
  124. data/web/locales/{zh-tw.yml → zh-TW.yml} +44 -9
  125. data/web/views/_footer.html.erb +32 -0
  126. data/web/views/_job_info.html.erb +115 -0
  127. data/web/views/_metrics_period_select.html.erb +15 -0
  128. data/web/views/_nav.html.erb +45 -0
  129. data/web/views/_paging.html.erb +26 -0
  130. data/web/views/_poll_link.html.erb +4 -0
  131. data/web/views/_summary.html.erb +40 -0
  132. data/web/views/busy.html.erb +151 -0
  133. data/web/views/dashboard.html.erb +104 -0
  134. data/web/views/dead.html.erb +38 -0
  135. data/web/views/filtering.html.erb +6 -0
  136. data/web/views/layout.html.erb +26 -0
  137. data/web/views/metrics.html.erb +85 -0
  138. data/web/views/metrics_for_job.html.erb +58 -0
  139. data/web/views/morgue.html.erb +69 -0
  140. data/web/views/profiles.html.erb +43 -0
  141. data/web/views/queue.html.erb +57 -0
  142. data/web/views/queues.html.erb +46 -0
  143. data/web/views/retries.html.erb +77 -0
  144. data/web/views/retry.html.erb +39 -0
  145. data/web/views/scheduled.html.erb +64 -0
  146. data/web/views/{scheduled_job_info.erb → scheduled_job_info.html.erb} +3 -3
  147. metadata +130 -61
  148. data/LICENSE +0 -9
  149. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  150. data/lib/sidekiq/delay.rb +0 -41
  151. data/lib/sidekiq/exception_handler.rb +0 -27
  152. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  153. data/lib/sidekiq/extensions/active_record.rb +0 -43
  154. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  155. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  156. data/lib/sidekiq/util.rb +0 -95
  157. data/lib/sidekiq/web/csrf_protection.rb +0 -180
  158. data/lib/sidekiq/worker.rb +0 -244
  159. data/web/assets/stylesheets/application-dark.css +0 -147
  160. data/web/assets/stylesheets/application-rtl.css +0 -246
  161. data/web/assets/stylesheets/application.css +0 -1053
  162. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  163. data/web/assets/stylesheets/bootstrap.css +0 -5
  164. data/web/locales/zh-cn.yml +0 -68
  165. data/web/views/_footer.erb +0 -20
  166. data/web/views/_job_info.erb +0 -89
  167. data/web/views/_nav.erb +0 -52
  168. data/web/views/_paging.erb +0 -23
  169. data/web/views/_poll_link.erb +0 -7
  170. data/web/views/_status.erb +0 -4
  171. data/web/views/_summary.erb +0 -40
  172. data/web/views/busy.erb +0 -132
  173. data/web/views/dashboard.erb +0 -83
  174. data/web/views/dead.erb +0 -34
  175. data/web/views/layout.erb +0 -42
  176. data/web/views/morgue.erb +0 -78
  177. data/web/views/queue.erb +0 -55
  178. data/web/views/queues.erb +0 -38
  179. data/web/views/retries.erb +0 -83
  180. data/web/views/retry.erb +0 -34
  181. data/web/views/scheduled.erb +0 -57
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/util"
4
3
  require "sidekiq/fetch"
5
4
  require "sidekiq/job_logger"
6
5
  require "sidekiq/job_retry"
6
+ require "sidekiq/profiler"
7
7
 
8
8
  module Sidekiq
9
9
  ##
@@ -11,33 +11,34 @@ module Sidekiq
11
11
  #
12
12
  # 1. fetches a job from Redis
13
13
  # 2. executes the job
14
- # a. instantiate the Worker
14
+ # a. instantiate the job class
15
15
  # b. run the middleware chain
16
16
  # c. call #perform
17
17
  #
18
- # A Processor can exit due to shutdown (processor_stopped)
19
- # or due to an error during job execution (processor_died)
18
+ # A Processor can exit due to shutdown or due to
19
+ # an error during job execution.
20
20
  #
21
21
  # If an error occurs in the job execution, the
22
22
  # Processor calls the Manager to create a new one
23
23
  # to replace itself and exits.
24
24
  #
25
25
  class Processor
26
- include Util
26
+ include Sidekiq::Component
27
27
 
28
28
  attr_reader :thread
29
29
  attr_reader :job
30
+ attr_reader :capsule
30
31
 
31
- def initialize(mgr, options)
32
- @mgr = mgr
32
+ def initialize(capsule, &block)
33
+ @config = @capsule = capsule
34
+ @callback = block
33
35
  @down = false
34
36
  @done = false
35
37
  @job = nil
36
38
  @thread = nil
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
39
+ @reloader = Sidekiq.default_configuration[:reloader]
40
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
41
+ @retrier = Sidekiq::JobRetry.new(capsule)
41
42
  end
42
43
 
43
44
  def terminate(wait = false)
@@ -58,34 +59,42 @@ 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
- @thread ||= safe_thread("processor", &method(:run))
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
73
+ # By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
74
+ # instead of the global pool in +Sidekiq::Config#redis_pool+.
75
+ Thread.current[:sidekiq_capsule] = @capsule
76
+
68
77
  process_one until @done
69
- @mgr.processor_stopped(self)
78
+ @callback.call(self)
70
79
  rescue Sidekiq::Shutdown
71
- @mgr.processor_stopped(self)
80
+ @callback.call(self)
72
81
  rescue Exception => ex
73
- @mgr.processor_died(self, ex)
82
+ @callback.call(self, ex)
74
83
  end
75
84
 
76
- def process_one
85
+ def process_one(&block)
77
86
  @job = fetch
78
87
  process(@job) if @job
79
88
  @job = nil
80
89
  end
81
90
 
82
91
  def get_one
83
- work = @strategy.retrieve_work
92
+ uow = capsule.fetcher.retrieve_work
84
93
  if @down
85
94
  logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }
86
95
  @down = nil
87
96
  end
88
- work
97
+ uow
89
98
  rescue Sidekiq::Shutdown
90
99
  rescue => ex
91
100
  handle_fetch_exception(ex)
@@ -104,13 +113,17 @@ module Sidekiq
104
113
  def handle_fetch_exception(ex)
105
114
  unless @down
106
115
  @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
107
- logger.error("Error fetching job: #{ex}")
108
116
  handle_exception(ex)
109
117
  end
110
118
  sleep(1)
111
119
  nil
112
120
  end
113
121
 
122
+ def profile(job, &block)
123
+ return yield unless job["profile"]
124
+ Sidekiq::Profiler.new(config).call(job, &block)
125
+ end
126
+
114
127
  def dispatch(job_hash, queue, jobstr)
115
128
  # since middleware can mutate the job hash
116
129
  # we need to clone it to report the original
@@ -124,16 +137,19 @@ module Sidekiq
124
137
  @retrier.global(jobstr, queue) do
125
138
  @job_logger.call(job_hash, queue) do
126
139
  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
- worker = klass.new
134
- worker.jid = job_hash["jid"]
135
- @retrier.local(worker, jobstr, queue) do
136
- yield worker
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
137
153
  end
138
154
  end
139
155
  end
@@ -142,58 +158,73 @@ module Sidekiq
142
158
  end
143
159
  end
144
160
 
145
- def process(work)
146
- jobstr = work.job
147
- queue = work.queue_name
161
+ IGNORE_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :never}
162
+ private_constant :IGNORE_SHUTDOWN_INTERRUPTS
163
+ ALLOW_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :immediate}
164
+ private_constant :ALLOW_SHUTDOWN_INTERRUPTS
165
+
166
+ def process(uow)
167
+ jobstr = uow.job
168
+ queue = uow.queue_name
148
169
 
149
170
  # Treat malformed JSON as a special case: job goes straight to the morgue.
150
171
  job_hash = nil
151
172
  begin
152
173
  job_hash = Sidekiq.load_json(jobstr)
153
174
  rescue => ex
175
+ now = Time.now.to_f
176
+ redis do |conn|
177
+ conn.multi do |xa|
178
+ xa.zadd("dead", now.to_s, jobstr)
179
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
180
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
181
+ end
182
+ end
154
183
  handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
155
- # we can't notify because the job isn't a valid hash payload.
156
- DeadSet.new.kill(jobstr, notify_failure: false)
157
- return work.acknowledge
184
+ return uow.acknowledge
158
185
  end
159
186
 
160
187
  ack = false
161
- begin
162
- dispatch(job_hash, queue, jobstr) do |worker|
163
- Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
164
- execute_job(worker, job_hash["args"])
188
+ Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
189
+ Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
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"])
193
+ end
165
194
  end
195
+ ack = true
196
+ rescue Sidekiq::Shutdown
197
+ # Had to force kill this job because it didn't finish
198
+ # within the timeout. Don't acknowledge the work since
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
205
+ rescue Sidekiq::JobRetry::Handled => h
206
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
207
+ # signals that we created a retry successfully. We can acknowledge the job.
208
+ ack = true
209
+ e = h.cause || h
210
+ handle_exception(e, {context: "Job raised exception", job: job_hash})
211
+ raise e
212
+ rescue Exception => ex
213
+ # Unexpected error! This is very bad and indicates an exception that got past
214
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
215
+ # so it can be rescued when using Sidekiq Pro.
216
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
217
+ raise ex
166
218
  end
167
- ack = true
168
- rescue Sidekiq::Shutdown
169
- # Had to force kill this job because it didn't finish
170
- # within the timeout. Don't acknowledge the work since
171
- # we didn't properly finish it.
172
- rescue Sidekiq::JobRetry::Handled => h
173
- # this is the common case: job raised error and Sidekiq::JobRetry::Handled
174
- # signals that we created a retry successfully. We can acknowlege the job.
175
- ack = true
176
- e = h.cause || h
177
- handle_exception(e, {context: "Job raised exception", job: job_hash, jobstr: jobstr})
178
- raise e
179
- rescue Exception => ex
180
- # Unexpected error! This is very bad and indicates an exception that got past
181
- # the retry subsystem (e.g. network partition). We won't acknowledge the job
182
- # so it can be rescued when using Sidekiq Pro.
183
- handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
184
- raise ex
185
219
  ensure
186
220
  if ack
187
- # We don't want a shutdown signal to interrupt job acknowledgment.
188
- Thread.handle_interrupt(Sidekiq::Shutdown => :never) do
189
- work.acknowledge
190
- end
221
+ uow.acknowledge
191
222
  end
192
223
  end
193
224
  end
194
225
 
195
- def execute_job(worker, cloned_args)
196
- worker.perform(*cloned_args)
226
+ def execute_job(instance, cloned_args)
227
+ instance.perform(*cloned_args)
197
228
  end
198
229
 
199
230
  # Ruby doesn't provide atomic counters out of the box so we'll
@@ -219,39 +250,39 @@ module Sidekiq
219
250
  end
220
251
 
221
252
  # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
222
- class SharedWorkerState
253
+ class SharedWorkState
223
254
  def initialize
224
- @worker_state = {}
255
+ @work_state = {}
225
256
  @lock = Mutex.new
226
257
  end
227
258
 
228
259
  def set(tid, hash)
229
- @lock.synchronize { @worker_state[tid] = hash }
260
+ @lock.synchronize { @work_state[tid] = hash }
230
261
  end
231
262
 
232
263
  def delete(tid)
233
- @lock.synchronize { @worker_state.delete(tid) }
264
+ @lock.synchronize { @work_state.delete(tid) }
234
265
  end
235
266
 
236
267
  def dup
237
- @lock.synchronize { @worker_state.dup }
268
+ @lock.synchronize { @work_state.dup }
238
269
  end
239
270
 
240
271
  def size
241
- @lock.synchronize { @worker_state.size }
272
+ @lock.synchronize { @work_state.size }
242
273
  end
243
274
 
244
275
  def clear
245
- @lock.synchronize { @worker_state.clear }
276
+ @lock.synchronize { @work_state.clear }
246
277
  end
247
278
  end
248
279
 
249
280
  PROCESSED = Counter.new
250
281
  FAILURE = Counter.new
251
- WORKER_STATE = SharedWorkerState.new
282
+ WORK_STATE = SharedWorkState.new
252
283
 
253
284
  def stats(jobstr, queue)
254
- WORKER_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
285
+ WORK_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
255
286
 
256
287
  begin
257
288
  yield
@@ -259,22 +290,9 @@ module Sidekiq
259
290
  FAILURE.incr
260
291
  raise
261
292
  ensure
262
- WORKER_STATE.delete(tid)
293
+ WORK_STATE.delete(tid)
263
294
  PROCESSED.incr
264
295
  end
265
296
  end
266
-
267
- def constantize(str)
268
- return Object.const_get(str) unless str.include?("::")
269
-
270
- names = str.split("::")
271
- names.shift if names.empty? || names.first.empty?
272
-
273
- names.inject(Object) do |constant, name|
274
- # the false flag limits search for name to under the constant namespace
275
- # which mimics Rails' behaviour
276
- constant.const_get(name, false)
277
- end
278
- end
279
297
  end
280
298
  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,50 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/worker"
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"
11
9
 
12
- def call
13
- @app.reloader.wrap do
14
- yield
10
+ class Rails < ::Rails::Engine
11
+ class Reloader
12
+ def initialize(app = ::Rails.application)
13
+ @app = app
14
+ end
15
+
16
+ def call
17
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
18
+ @app.reloader.wrap(**params) do
19
+ yield
20
+ end
21
+ end
22
+
23
+ def inspect
24
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
15
25
  end
16
- end
17
26
 
18
- def inspect
19
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
27
+ def to_hash
28
+ {app: @app.class.name}
29
+ end
20
30
  end
21
- end
22
31
 
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::Worker::Options unless respond_to?(:sidekiq_options)
32
+ initializer "sidekiq.backtrace_cleaner" do
33
+ Sidekiq.configure_server do |config|
34
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
35
+ end
37
36
  end
38
- end
39
37
 
40
- # This hook happens after all initializers are run, just before returning
41
- # from config/environment.rb back to sidekiq/cli.rb.
42
- #
43
- # None of this matters on the client-side, only within the Sidekiq process itself.
44
- config.after_initialize do
45
- Sidekiq.configure_server do |_|
46
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
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
56
+ end
57
+ end
47
58
  end
48
59
  end
60
+ rescue Gem::LoadError
61
+ # Rails not available or version requirement not met
49
62
  end
50
63
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis_client"
4
+ require "redis_client/decorator"
5
+
6
+ module Sidekiq
7
+ class RedisClientAdapter
8
+ BaseError = RedisClient::Error
9
+ CommandError = RedisClient::CommandError
10
+
11
+ # You can add/remove items or clear the whole thing if you don't want deprecation warnings.
12
+ DEPRECATED_COMMANDS = %i[rpoplpush zrangebyscore zrevrange zrevrangebyscore getset hmset setex setnx].to_set
13
+
14
+ module CompatMethods
15
+ def info
16
+ @client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
17
+ end
18
+
19
+ def evalsha(sha, keys, argv)
20
+ @client.call("EVALSHA", sha, keys.size, *keys, *argv)
21
+ end
22
+
23
+ # this is the set of Redis commands used by Sidekiq. Not guaranteed
24
+ # to be comprehensive, we use this as a performance enhancement to
25
+ # avoid calling method_missing on most commands
26
+ USED_COMMANDS = %w[bitfield bitfield_ro del exists expire flushdb
27
+ get hdel hget hgetall hincrby hlen hmget hset hsetnx incr incrby
28
+ lindex llen lmove lpop lpush lrange lrem mget mset ping pttl
29
+ publish rpop rpush sadd scard script set sismember smembers
30
+ srem ttl type unlink zadd zcard zincrby zrange zrem
31
+ zremrangebyrank zremrangebyscore]
32
+
33
+ USED_COMMANDS.each do |name|
34
+ define_method(name) do |*args, **kwargs|
35
+ @client.call(name, *args, **kwargs)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # this allows us to use methods like `conn.hmset(...)` instead of having to use
42
+ # redis-client's native `conn.call("hmset", ...)`
43
+ def method_missing(*args, &block)
44
+ warn("[sidekiq#5788] Redis has deprecated the `#{args.first}`command, called at #{caller(1..1)}") if DEPRECATED_COMMANDS.include?(args.first)
45
+ @client.call(*args, *block)
46
+ end
47
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
48
+
49
+ def respond_to_missing?(name, include_private = false)
50
+ super # Appease the linter. We can't tell what is a valid command.
51
+ end
52
+ end
53
+
54
+ CompatClient = RedisClient::Decorator.create(CompatMethods)
55
+
56
+ class CompatClient
57
+ def config
58
+ @client.config
59
+ end
60
+ end
61
+
62
+ def initialize(options)
63
+ opts = client_opts(options)
64
+ @config = if opts.key?(:sentinels)
65
+ RedisClient.sentinel(**opts)
66
+ elsif opts.key?(:nodes)
67
+ # Sidekiq does not support Redis clustering but Sidekiq Enterprise's
68
+ # rate limiters are cluster-safe so we can scale to millions
69
+ # of rate limiters using a Redis cluster. This requires the
70
+ # `redis-cluster-client` gem.
71
+ # Sidekiq::Limiter.redis = { nodes: [...] }
72
+ RedisClient.cluster(**opts)
73
+ else
74
+ RedisClient.config(**opts)
75
+ end
76
+ end
77
+
78
+ def new_client
79
+ CompatClient.new(@config.new_client)
80
+ end
81
+
82
+ private
83
+
84
+ def client_opts(options)
85
+ opts = options.dup
86
+
87
+ if opts[:namespace]
88
+ raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature is no longer supported in Sidekiq 7+. See https://github.com/sidekiq/sidekiq/blob/main/docs/7.0-Upgrade.md#redis-namespace."
89
+ end
90
+
91
+ opts.delete(:size)
92
+ opts.delete(:pool_timeout)
93
+
94
+ if opts[:network_timeout]
95
+ opts[:timeout] = opts[:network_timeout]
96
+ opts.delete(:network_timeout)
97
+ end
98
+
99
+ opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
100
+ opts[:role] = opts[:role].to_sym if opts.key?(:role)
101
+ opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
102
+
103
+ # Issue #3303, redis-rb will silently retry an operation.
104
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
105
+ # is performed twice but I believe this is much, much rarer
106
+ # than the reconnect silently fixing a problem; we keep it
107
+ # on by default.
108
+ opts[:reconnect_attempts] ||= 1
109
+
110
+ # Identify ourselves to Redis via CLIENT SETINFO so connections
111
+ # are distinguishable in CLIENT LIST / CLIENT INFO output.
112
+ opts[:driver_info] ||= "sidekiq_v#{Sidekiq::VERSION}"
113
+
114
+ opts
115
+ end
116
+ end
117
+ end