sidekiq 4.2.10 → 7.3.10

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 (159) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +932 -7
  3. data/LICENSE.txt +9 -0
  4. data/README.md +49 -50
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +218 -116
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  10. data/lib/generators/sidekiq/job_generator.rb +59 -0
  11. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  12. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  13. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  14. data/lib/sidekiq/api.rb +710 -322
  15. data/lib/sidekiq/capsule.rb +132 -0
  16. data/lib/sidekiq/cli.rb +268 -248
  17. data/lib/sidekiq/client.rb +153 -101
  18. data/lib/sidekiq/component.rb +90 -0
  19. data/lib/sidekiq/config.rb +311 -0
  20. data/lib/sidekiq/deploy.rb +64 -0
  21. data/lib/sidekiq/embedded.rb +63 -0
  22. data/lib/sidekiq/fetch.rb +50 -42
  23. data/lib/sidekiq/iterable_job.rb +55 -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 +294 -0
  29. data/lib/sidekiq/job.rb +385 -0
  30. data/lib/sidekiq/job_logger.rb +52 -0
  31. data/lib/sidekiq/job_retry.rb +305 -0
  32. data/lib/sidekiq/job_util.rb +109 -0
  33. data/lib/sidekiq/launcher.rb +208 -108
  34. data/lib/sidekiq/logger.rb +131 -0
  35. data/lib/sidekiq/manager.rb +43 -47
  36. data/lib/sidekiq/metrics/query.rb +158 -0
  37. data/lib/sidekiq/metrics/shared.rb +106 -0
  38. data/lib/sidekiq/metrics/tracking.rb +148 -0
  39. data/lib/sidekiq/middleware/chain.rb +113 -56
  40. data/lib/sidekiq/middleware/current_attributes.rb +128 -0
  41. data/lib/sidekiq/middleware/i18n.rb +9 -7
  42. data/lib/sidekiq/middleware/modules.rb +23 -0
  43. data/lib/sidekiq/monitor.rb +147 -0
  44. data/lib/sidekiq/paginator.rb +33 -15
  45. data/lib/sidekiq/processor.rb +188 -98
  46. data/lib/sidekiq/rails.rb +53 -92
  47. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  48. data/lib/sidekiq/redis_connection.rb +86 -77
  49. data/lib/sidekiq/ring_buffer.rb +32 -0
  50. data/lib/sidekiq/scheduled.rb +140 -51
  51. data/lib/sidekiq/sd_notify.rb +149 -0
  52. data/lib/sidekiq/systemd.rb +26 -0
  53. data/lib/sidekiq/testing/inline.rb +6 -5
  54. data/lib/sidekiq/testing.rb +95 -85
  55. data/lib/sidekiq/transaction_aware_client.rb +59 -0
  56. data/lib/sidekiq/version.rb +7 -1
  57. data/lib/sidekiq/web/action.rb +40 -18
  58. data/lib/sidekiq/web/application.rb +189 -89
  59. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  60. data/lib/sidekiq/web/helpers.rb +239 -101
  61. data/lib/sidekiq/web/router.rb +28 -21
  62. data/lib/sidekiq/web.rb +123 -110
  63. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  64. data/lib/sidekiq.rb +97 -185
  65. data/sidekiq.gemspec +26 -27
  66. data/web/assets/images/apple-touch-icon.png +0 -0
  67. data/web/assets/javascripts/application.js +157 -61
  68. data/web/assets/javascripts/base-charts.js +106 -0
  69. data/web/assets/javascripts/chart.min.js +13 -0
  70. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  71. data/web/assets/javascripts/dashboard-charts.js +194 -0
  72. data/web/assets/javascripts/dashboard.js +43 -280
  73. data/web/assets/javascripts/metrics.js +298 -0
  74. data/web/assets/stylesheets/application-dark.css +147 -0
  75. data/web/assets/stylesheets/application-rtl.css +163 -0
  76. data/web/assets/stylesheets/application.css +176 -196
  77. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  78. data/web/assets/stylesheets/bootstrap.css +2 -2
  79. data/web/locales/ar.yml +87 -0
  80. data/web/locales/cs.yml +62 -62
  81. data/web/locales/da.yml +60 -53
  82. data/web/locales/de.yml +65 -53
  83. data/web/locales/el.yml +43 -24
  84. data/web/locales/en.yml +88 -64
  85. data/web/locales/es.yml +70 -53
  86. data/web/locales/fa.yml +65 -64
  87. data/web/locales/fr.yml +82 -62
  88. data/web/locales/gd.yml +98 -0
  89. data/web/locales/he.yml +80 -0
  90. data/web/locales/hi.yml +59 -59
  91. data/web/locales/it.yml +85 -54
  92. data/web/locales/ja.yml +74 -62
  93. data/web/locales/ko.yml +52 -52
  94. data/web/locales/lt.yml +83 -0
  95. data/web/locales/nb.yml +61 -61
  96. data/web/locales/nl.yml +52 -52
  97. data/web/locales/pl.yml +45 -45
  98. data/web/locales/pt-br.yml +82 -55
  99. data/web/locales/pt.yml +51 -51
  100. data/web/locales/ru.yml +68 -63
  101. data/web/locales/sv.yml +53 -53
  102. data/web/locales/ta.yml +60 -60
  103. data/web/locales/tr.yml +100 -0
  104. data/web/locales/uk.yml +85 -61
  105. data/web/locales/ur.yml +80 -0
  106. data/web/locales/vi.yml +83 -0
  107. data/web/locales/zh-cn.yml +42 -16
  108. data/web/locales/zh-tw.yml +41 -8
  109. data/web/views/_footer.erb +20 -3
  110. data/web/views/_job_info.erb +21 -4
  111. data/web/views/_metrics_period_select.erb +12 -0
  112. data/web/views/_nav.erb +5 -19
  113. data/web/views/_paging.erb +3 -1
  114. data/web/views/_poll_link.erb +3 -6
  115. data/web/views/_summary.erb +7 -7
  116. data/web/views/busy.erb +85 -31
  117. data/web/views/dashboard.erb +53 -20
  118. data/web/views/dead.erb +3 -3
  119. data/web/views/filtering.erb +6 -0
  120. data/web/views/layout.erb +17 -6
  121. data/web/views/metrics.erb +90 -0
  122. data/web/views/metrics_for_job.erb +59 -0
  123. data/web/views/morgue.erb +15 -16
  124. data/web/views/queue.erb +35 -25
  125. data/web/views/queues.erb +20 -4
  126. data/web/views/retries.erb +19 -16
  127. data/web/views/retry.erb +3 -3
  128. data/web/views/scheduled.erb +19 -17
  129. metadata +103 -194
  130. data/.github/contributing.md +0 -32
  131. data/.github/issue_template.md +0 -9
  132. data/.gitignore +0 -12
  133. data/.travis.yml +0 -18
  134. data/3.0-Upgrade.md +0 -70
  135. data/4.0-Upgrade.md +0 -53
  136. data/COMM-LICENSE +0 -95
  137. data/Ent-Changes.md +0 -173
  138. data/Gemfile +0 -29
  139. data/LICENSE +0 -9
  140. data/Pro-2.0-Upgrade.md +0 -138
  141. data/Pro-3.0-Upgrade.md +0 -44
  142. data/Pro-Changes.md +0 -628
  143. data/Rakefile +0 -12
  144. data/bin/sidekiqctl +0 -99
  145. data/code_of_conduct.md +0 -50
  146. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  147. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  148. data/lib/sidekiq/core_ext.rb +0 -119
  149. data/lib/sidekiq/exception_handler.rb +0 -31
  150. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  151. data/lib/sidekiq/extensions/active_record.rb +0 -40
  152. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  153. data/lib/sidekiq/extensions/generic_proxy.rb +0 -25
  154. data/lib/sidekiq/logging.rb +0 -106
  155. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  156. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  157. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  158. data/lib/sidekiq/util.rb +0 -63
  159. data/lib/sidekiq/worker.rb +0 -121
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/util'
3
- require 'sidekiq/fetch'
4
- require 'thread'
5
- require 'concurrent/map'
6
- require 'concurrent/atomic/atomic_fixnum'
2
+
3
+ require "sidekiq/fetch"
4
+ require "sidekiq/job_logger"
5
+ require "sidekiq/job_retry"
7
6
 
8
7
  module Sidekiq
9
8
  ##
@@ -11,44 +10,45 @@ module Sidekiq
11
10
  #
12
11
  # 1. fetches a job from Redis
13
12
  # 2. executes the job
14
- # a. instantiate the Worker
13
+ # a. instantiate the job class
15
14
  # b. run the middleware chain
16
15
  # c. call #perform
17
16
  #
18
- # A Processor can exit due to shutdown (processor_stopped)
19
- # or due to an error during job execution (processor_died)
17
+ # A Processor can exit due to shutdown or due to
18
+ # an error during job execution.
20
19
  #
21
20
  # If an error occurs in the job execution, the
22
21
  # Processor calls the Manager to create a new one
23
22
  # to replace itself and exits.
24
23
  #
25
24
  class Processor
26
-
27
- include Util
25
+ include Sidekiq::Component
28
26
 
29
27
  attr_reader :thread
30
28
  attr_reader :job
29
+ attr_reader :capsule
31
30
 
32
- def initialize(mgr)
33
- @mgr = mgr
31
+ def initialize(capsule, &block)
32
+ @config = @capsule = capsule
33
+ @callback = block
34
34
  @down = false
35
35
  @done = false
36
36
  @job = nil
37
37
  @thread = nil
38
- @strategy = (mgr.options[:fetch] || Sidekiq::BasicFetch).new(mgr.options)
39
- @reloader = Sidekiq.options[:reloader]
40
- @executor = Sidekiq.options[:executor]
38
+ @reloader = Sidekiq.default_configuration[:reloader]
39
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
40
+ @retrier = Sidekiq::JobRetry.new(capsule)
41
41
  end
42
42
 
43
- def terminate(wait=false)
43
+ def terminate(wait = false)
44
44
  @done = true
45
- return if !@thread
45
+ return unless @thread
46
46
  @thread.value if wait
47
47
  end
48
48
 
49
- def kill(wait=false)
49
+ def kill(wait = false)
50
50
  @done = true
51
- return if !@thread
51
+ return unless @thread
52
52
  # unlike the other actors, terminate does not wait
53
53
  # for the thread to finish because we don't know how
54
54
  # long the job will take to finish. Instead we
@@ -58,40 +58,45 @@ module Sidekiq
58
58
  @thread.value if wait
59
59
  end
60
60
 
61
+ def stopping?
62
+ @done
63
+ end
64
+
61
65
  def start
62
- @thread ||= safe_thread("processor", &method(:run))
66
+ @thread ||= safe_thread("#{config.name}/processor", &method(:run))
63
67
  end
64
68
 
65
69
  private unless $TESTING
66
70
 
67
71
  def run
68
- begin
69
- while !@done
70
- process_one
71
- end
72
- @mgr.processor_stopped(self)
73
- rescue Sidekiq::Shutdown
74
- @mgr.processor_stopped(self)
75
- rescue Exception => ex
76
- @mgr.processor_died(self, ex)
77
- end
72
+ # By setting this thread-local, Sidekiq.redis will access +Sidekiq::Capsule#redis_pool+
73
+ # instead of the global pool in +Sidekiq::Config#redis_pool+.
74
+ Thread.current[:sidekiq_capsule] = @capsule
75
+
76
+ process_one until @done
77
+ @callback.call(self)
78
+ rescue Sidekiq::Shutdown
79
+ @callback.call(self)
80
+ rescue Exception => ex
81
+ @callback.call(self, ex)
78
82
  end
79
83
 
80
- def process_one
84
+ def process_one(&block)
81
85
  @job = fetch
82
86
  process(@job) if @job
83
87
  @job = nil
84
88
  end
85
89
 
86
90
  def get_one
87
- begin
88
- work = @strategy.retrieve_work
89
- (logger.info { "Redis is online, #{Time.now - @down} sec downtime" }; @down = nil) if @down
90
- work
91
- rescue Sidekiq::Shutdown
92
- rescue => ex
93
- handle_fetch_exception(ex)
91
+ uow = capsule.fetcher.retrieve_work
92
+ if @down
93
+ logger.info { "Redis is online, #{::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - @down} sec downtime" }
94
+ @down = nil
94
95
  end
96
+ uow
97
+ rescue Sidekiq::Shutdown
98
+ rescue => ex
99
+ handle_fetch_exception(ex)
95
100
  end
96
101
 
97
102
  def fetch
@@ -105,97 +110,182 @@ module Sidekiq
105
110
  end
106
111
 
107
112
  def handle_fetch_exception(ex)
108
- if !@down
109
- @down = Time.now
113
+ unless @down
114
+ @down = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
110
115
  logger.error("Error fetching job: #{ex}")
111
- ex.backtrace.each do |bt|
112
- logger.error(bt)
113
- end
116
+ handle_exception(ex)
114
117
  end
115
118
  sleep(1)
116
119
  nil
117
120
  end
118
121
 
119
- def process(work)
120
- jobstr = work.job
121
- queue = work.queue_name
122
+ def dispatch(job_hash, queue, jobstr)
123
+ # since middleware can mutate the job hash
124
+ # we need to clone it to report the original
125
+ # job structure to the Web UI
126
+ # or to push back to redis when retrying.
127
+ # To avoid costly and, most of the time, useless cloning here,
128
+ # we pass original String of JSON to respected methods
129
+ # to re-parse it there if we need access to the original, untouched job
122
130
 
123
- ack = false
124
- begin
125
- job_hash = Sidekiq.load_json(jobstr)
126
- @reloader.call do
127
- klass = job_hash['class'.freeze].constantize
128
- worker = klass.new
129
- worker.jid = job_hash['jid'.freeze]
130
-
131
- stats(worker, job_hash, queue) do
132
- Sidekiq::Logging.with_context(log_context(job_hash)) do
133
- ack = true
134
- Sidekiq.server_middleware.invoke(worker, job_hash, queue) do
135
- @executor.call do
136
- # Only ack if we either attempted to start this job or
137
- # successfully completed it. This prevents us from
138
- # losing jobs if a middleware raises an exception before yielding
139
- execute_job(worker, cloned(job_hash['args'.freeze]))
131
+ @job_logger.prepare(job_hash) do
132
+ @retrier.global(jobstr, queue) do
133
+ @job_logger.call(job_hash, queue) do
134
+ 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
146
  end
141
147
  end
142
148
  end
143
149
  end
150
+ end
151
+ end
152
+ end
153
+
154
+ IGNORE_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :never}
155
+ private_constant :IGNORE_SHUTDOWN_INTERRUPTS
156
+ ALLOW_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :immediate}
157
+ private_constant :ALLOW_SHUTDOWN_INTERRUPTS
158
+
159
+ def process(uow)
160
+ jobstr = uow.job
161
+ queue = uow.queue_name
162
+
163
+ # Treat malformed JSON as a special case: job goes straight to the morgue.
164
+ job_hash = nil
165
+ begin
166
+ job_hash = Sidekiq.load_json(jobstr)
167
+ rescue => ex
168
+ handle_exception(ex, {context: "Invalid JSON for job", jobstr: jobstr})
169
+ now = Time.now.to_f
170
+ redis do |conn|
171
+ conn.multi do |xa|
172
+ xa.zadd("dead", now.to_s, jobstr)
173
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
174
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
175
+ end
176
+ end
177
+ return uow.acknowledge
178
+ end
179
+
180
+ ack = false
181
+ Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
182
+ Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
183
+ dispatch(job_hash, queue, jobstr) do |instance|
184
+ config.server_middleware.invoke(instance, job_hash, queue) do
185
+ execute_job(instance, job_hash["args"])
186
+ end
187
+ end
188
+ ack = true
189
+ rescue Sidekiq::Shutdown
190
+ # Had to force kill this job because it didn't finish
191
+ # within the timeout. Don't acknowledge the work since
192
+ # we didn't properly finish it.
193
+ rescue Sidekiq::JobRetry::Skip => s
194
+ # Skip means we handled this error elsewhere. We don't
195
+ # need to log or report the error.
144
196
  ack = true
197
+ raise s
198
+ rescue Sidekiq::JobRetry::Handled => h
199
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
200
+ # signals that we created a retry successfully. We can acknowledge the job.
201
+ ack = true
202
+ e = h.cause || h
203
+ handle_exception(e, {context: "Job raised exception", job: job_hash})
204
+ raise e
205
+ rescue Exception => ex
206
+ # Unexpected error! This is very bad and indicates an exception that got past
207
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
208
+ # so it can be rescued when using Sidekiq Pro.
209
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
210
+ raise ex
145
211
  end
146
- rescue Sidekiq::Shutdown
147
- # Had to force kill this job because it didn't finish
148
- # within the timeout. Don't acknowledge the work since
149
- # we didn't properly finish it.
150
- ack = false
151
- rescue Exception => ex
152
- handle_exception(ex, { :context => "Job raised exception", :job => job_hash, :jobstr => jobstr })
153
- raise
154
212
  ensure
155
- work.acknowledge if ack
213
+ if ack
214
+ uow.acknowledge
215
+ end
156
216
  end
157
217
  end
158
218
 
159
- # If we're using a wrapper class, like ActiveJob, use the "wrapped"
160
- # attribute to expose the underlying thing.
161
- def log_context(item)
162
- klass = item['wrapped'.freeze] || item['class'.freeze]
163
- "#{klass} JID-#{item['jid'.freeze]}#{" BID-#{item['bid'.freeze]}" if item['bid'.freeze]}"
219
+ def execute_job(instance, cloned_args)
220
+ instance.perform(*cloned_args)
164
221
  end
165
222
 
166
- def execute_job(worker, cloned_args)
167
- worker.perform(*cloned_args)
223
+ # Ruby doesn't provide atomic counters out of the box so we'll
224
+ # implement something simple ourselves.
225
+ # https://bugs.ruby-lang.org/issues/14706
226
+ class Counter
227
+ def initialize
228
+ @value = 0
229
+ @lock = Mutex.new
230
+ end
231
+
232
+ def incr(amount = 1)
233
+ @lock.synchronize { @value += amount }
234
+ end
235
+
236
+ def reset
237
+ @lock.synchronize {
238
+ val = @value
239
+ @value = 0
240
+ val
241
+ }
242
+ end
168
243
  end
169
244
 
170
- def thread_identity
171
- @str ||= Thread.current.object_id.to_s(36)
245
+ # jruby's Hash implementation is not threadsafe, so we wrap it in a mutex here
246
+ class SharedWorkState
247
+ def initialize
248
+ @work_state = {}
249
+ @lock = Mutex.new
250
+ end
251
+
252
+ def set(tid, hash)
253
+ @lock.synchronize { @work_state[tid] = hash }
254
+ end
255
+
256
+ def delete(tid)
257
+ @lock.synchronize { @work_state.delete(tid) }
258
+ end
259
+
260
+ def dup
261
+ @lock.synchronize { @work_state.dup }
262
+ end
263
+
264
+ def size
265
+ @lock.synchronize { @work_state.size }
266
+ end
267
+
268
+ def clear
269
+ @lock.synchronize { @work_state.clear }
270
+ end
172
271
  end
173
272
 
174
- WORKER_STATE = Concurrent::Map.new
175
- PROCESSED = Concurrent::AtomicFixnum.new
176
- FAILURE = Concurrent::AtomicFixnum.new
273
+ PROCESSED = Counter.new
274
+ FAILURE = Counter.new
275
+ WORK_STATE = SharedWorkState.new
177
276
 
178
- def stats(worker, job_hash, queue)
179
- tid = thread_identity
180
- WORKER_STATE[tid] = {:queue => queue, :payload => cloned(job_hash), :run_at => Time.now.to_i }
277
+ def stats(jobstr, queue)
278
+ WORK_STATE.set(tid, {queue: queue, payload: jobstr, run_at: Time.now.to_i})
181
279
 
182
280
  begin
183
281
  yield
184
282
  rescue Exception
185
- FAILURE.increment
283
+ FAILURE.incr
186
284
  raise
187
285
  ensure
188
- WORKER_STATE.delete(tid)
189
- PROCESSED.increment
286
+ WORK_STATE.delete(tid)
287
+ PROCESSED.incr
190
288
  end
191
289
  end
192
-
193
- # Deep clone the arguments passed to the worker so that if
194
- # the job fails, what is pushed back onto Redis hasn't
195
- # been mutated by the worker.
196
- def cloned(ary)
197
- Marshal.load(Marshal.dump(ary))
198
- end
199
-
200
290
  end
201
291
  end
data/lib/sidekiq/rails.rb CHANGED
@@ -1,123 +1,84 @@
1
1
  # frozen_string_literal: true
2
- module Sidekiq
3
- def self.hook_rails!
4
- return if defined?(@delay_removed)
5
-
6
- ActiveSupport.on_load(:active_record) do
7
- include Sidekiq::Extensions::ActiveRecord
8
- end
9
2
 
10
- ActiveSupport.on_load(:action_mailer) do
11
- extend Sidekiq::Extensions::ActionMailer
12
- end
13
-
14
- Module.__send__(:include, Sidekiq::Extensions::Klass)
15
- end
3
+ require "sidekiq/job"
4
+ require "rails"
16
5
 
17
- # Removes the generic aliases which MAY clash with names of already
18
- # created methods by other applications. The methods `sidekiq_delay`,
19
- # `sidekiq_delay_for` and `sidekiq_delay_until` can be used instead.
20
- def self.remove_delay!
21
- @delay_removed = true
6
+ module Sidekiq
7
+ module ActiveJob
8
+ # @api private
9
+ class Wrapper
10
+ include Sidekiq::Job
22
11
 
23
- [Extensions::ActiveRecord,
24
- Extensions::ActionMailer,
25
- Extensions::Klass].each do |mod|
26
- mod.module_eval do
27
- remove_method :delay if respond_to?(:delay)
28
- remove_method :delay_for if respond_to?(:delay_for)
29
- remove_method :delay_until if respond_to?(:delay_until)
12
+ def perform(job_data)
13
+ ::ActiveJob::Base.execute(job_data.merge("provider_job_id" => jid))
30
14
  end
31
15
  end
32
16
  end
33
17
 
34
18
  class Rails < ::Rails::Engine
35
- # We need to setup this up before any application configuration which might
36
- # change Sidekiq middleware.
37
- #
38
- # This hook happens after `Rails::Application` is inherited within
39
- # config/application.rb and before config is touched, usually within the
40
- # class block. Definitely before config/environments/*.rb and
41
- # config/initializers/*.rb.
42
- config.before_configuration do
43
- if ::Rails::VERSION::MAJOR < 5 && defined?(::ActiveRecord)
44
- Sidekiq.server_middleware do |chain|
45
- require 'sidekiq/middleware/server/active_record'
46
- chain.add Sidekiq::Middleware::Server::ActiveRecord
47
- end
48
- end
49
- end
50
-
51
- initializer 'sidekiq' do
52
- Sidekiq.hook_rails!
53
- end
54
-
55
- config.after_initialize do
56
- # This hook happens after all initializers are run, just before returning
57
- # from config/environment.rb back to sidekiq/cli.rb.
58
- # We have to add the reloader after initialize to see if cache_classes has
59
- # been turned on.
60
- #
61
- # None of this matters on the client-side, only within the Sidekiq process itself.
62
- #
63
- Sidekiq.configure_server do |_|
64
- if ::Rails::VERSION::MAJOR >= 5
65
- # The reloader also takes care of ActiveRecord but is incompatible with
66
- # the ActiveRecord middleware so make sure it's not in the chain already.
67
- if defined?(Sidekiq::Middleware::Server::ActiveRecord) && Sidekiq.server_middleware.exists?(Sidekiq::Middleware::Server::ActiveRecord)
68
- raise ArgumentError, "You are using the Sidekiq ActiveRecord middleware and the new Rails 5 reloader which are incompatible. Please remove the ActiveRecord middleware from your Sidekiq middleware configuration."
69
- elsif ::Rails.application.config.cache_classes
70
- # The reloader API has proven to be troublesome under load in production.
71
- # We won't use it at all when classes are cached, see #3154
72
- Sidekiq.logger.debug { "Autoload disabled in #{::Rails.env}, Sidekiq will not reload changed classes" }
73
- Sidekiq.options[:executor] = Sidekiq::Rails::Executor.new
74
- else
75
- Sidekiq.logger.debug { "Enabling Rails 5+ live code reloading, so hot!" }
76
- Sidekiq.options[:reloader] = Sidekiq::Rails::Reloader.new
77
- Psych::Visitors::ToRuby.prepend(Sidekiq::Rails::PsychAutoload)
78
- end
79
- end
80
- end
81
- end
82
-
83
- class Executor
19
+ class Reloader
84
20
  def initialize(app = ::Rails.application)
85
21
  @app = app
86
22
  end
87
23
 
88
24
  def call
89
- @app.executor.wrap do
25
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "job.sidekiq"} : {}
26
+ @app.reloader.wrap(**params) do
90
27
  yield
91
28
  end
92
29
  end
93
30
 
94
31
  def inspect
95
- "#<Sidekiq::Rails::Executor @app=#{@app.class.name}>"
32
+ "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
96
33
  end
97
- end
98
34
 
99
- class Reloader
100
- def initialize(app = ::Rails.application)
101
- @app = app
35
+ def to_hash
36
+ {app: @app.class.name}
102
37
  end
38
+ end
103
39
 
104
- def call
105
- @app.reloader.wrap do
106
- yield
107
- end
40
+ # By including the Options module, we allow AJs to directly control sidekiq features
41
+ # via the *sidekiq_options* class method and, for instance, not use AJ's retry system.
42
+ # AJ retries don't show up in the Sidekiq UI Retries tab, don't save any error data, can't be
43
+ # manually retried, don't automatically die, etc.
44
+ #
45
+ # class SomeJob < ActiveJob::Base
46
+ # queue_as :default
47
+ # sidekiq_options retry: 3, backtrace: 10
48
+ # def perform
49
+ # end
50
+ # end
51
+ initializer "sidekiq.active_job_integration" do
52
+ ActiveSupport.on_load(:active_job) do
53
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
54
+ include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
108
55
  end
56
+ end
109
57
 
110
- def inspect
111
- "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
58
+ initializer "sidekiq.backtrace_cleaner" do
59
+ Sidekiq.configure_server do |config|
60
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
112
61
  end
113
62
  end
114
63
 
115
- module PsychAutoload
116
- def resolve_class(klass_name)
117
- klass_name && klass_name.constantize
118
- rescue NameError
119
- super
64
+ # This hook happens after all initializers are run, just before returning
65
+ # from config/environment.rb back to sidekiq/cli.rb.
66
+ #
67
+ # None of this matters on the client-side, only within the Sidekiq process itself.
68
+ config.after_initialize do
69
+ Sidekiq.configure_server do |config|
70
+ config[:reloader] = Sidekiq::Rails::Reloader.new
71
+
72
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
73
+ # it will appear in the Sidekiq console with all of the job context.
74
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
75
+ if ::Rails.logger.respond_to?(:broadcast_to)
76
+ ::Rails.logger.broadcast_to(config.logger)
77
+ else
78
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
79
+ end
80
+ end
120
81
  end
121
82
  end
122
- end if defined?(::Rails)
83
+ end
123
84
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "redis_client"
5
+ require "redis_client/decorator"
6
+
7
+ module Sidekiq
8
+ class RedisClientAdapter
9
+ BaseError = RedisClient::Error
10
+ CommandError = RedisClient::CommandError
11
+
12
+ # You can add/remove items or clear the whole thing if you don't want deprecation warnings.
13
+ DEPRECATED_COMMANDS = %i[rpoplpush zrangebyscore zrevrange zrevrangebyscore getset hmset setex setnx].to_set
14
+
15
+ module CompatMethods
16
+ def info
17
+ @client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
18
+ end
19
+
20
+ def evalsha(sha, keys, argv)
21
+ @client.call("EVALSHA", sha, keys.size, *keys, *argv)
22
+ end
23
+
24
+ # this is the set of Redis commands used by Sidekiq. Not guaranteed
25
+ # to be comprehensive, we use this as a performance enhancement to
26
+ # avoid calling method_missing on most commands
27
+ USED_COMMANDS = %w[bitfield bitfield_ro del exists expire flushdb
28
+ get hdel hget hgetall hincrby hlen hmget hset hsetnx incr incrby
29
+ lindex llen lmove lpop lpush lrange lrem mget mset ping pttl
30
+ publish rpop rpush sadd scard script set sismember smembers
31
+ srem ttl type unlink zadd zcard zincrby zrange zrem
32
+ zremrangebyrank zremrangebyscore]
33
+
34
+ USED_COMMANDS.each do |name|
35
+ define_method(name) do |*args, **kwargs|
36
+ @client.call(name, *args, **kwargs)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # this allows us to use methods like `conn.hmset(...)` instead of having to use
43
+ # redis-client's native `conn.call("hmset", ...)`
44
+ def method_missing(*args, &block)
45
+ warn("[sidekiq#5788] Redis has deprecated the `#{args.first}`command, called at #{caller(1..1)}") if DEPRECATED_COMMANDS.include?(args.first)
46
+ @client.call(*args, *block)
47
+ end
48
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
49
+
50
+ def respond_to_missing?(name, include_private = false)
51
+ super # Appease the linter. We can't tell what is a valid command.
52
+ end
53
+ end
54
+
55
+ CompatClient = RedisClient::Decorator.create(CompatMethods)
56
+
57
+ class CompatClient
58
+ def config
59
+ @client.config
60
+ end
61
+ end
62
+
63
+ def initialize(options)
64
+ opts = client_opts(options)
65
+ @config = if opts.key?(:sentinels)
66
+ RedisClient.sentinel(**opts)
67
+ elsif opts.key?(:nodes)
68
+ # Sidekiq does not support Redis clustering but Sidekiq Enterprise's
69
+ # rate limiters are cluster-safe so we can scale to millions
70
+ # of rate limiters using a Redis cluster. This requires the
71
+ # `redis-cluster-client` gem.
72
+ # Sidekiq::Limiter.redis = { nodes: [...] }
73
+ RedisClient.cluster(**opts)
74
+ else
75
+ RedisClient.config(**opts)
76
+ end
77
+ end
78
+
79
+ def new_client
80
+ CompatClient.new(@config.new_client)
81
+ end
82
+
83
+ private
84
+
85
+ def client_opts(options)
86
+ opts = options.dup
87
+
88
+ if opts[:namespace]
89
+ 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."
90
+ end
91
+
92
+ opts.delete(:size)
93
+ opts.delete(:pool_timeout)
94
+
95
+ if opts[:network_timeout]
96
+ opts[:timeout] = opts[:network_timeout]
97
+ opts.delete(:network_timeout)
98
+ end
99
+
100
+ opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
101
+ opts[:role] = opts[:role].to_sym if opts.key?(:role)
102
+ opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
103
+
104
+ # Issue #3303, redis-rb will silently retry an operation.
105
+ # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
106
+ # is performed twice but I believe this is much, much rarer
107
+ # than the reconnect silently fixing a problem; we keep it
108
+ # on by default.
109
+ opts[:reconnect_attempts] ||= 1
110
+
111
+ opts
112
+ end
113
+ end
114
+ end