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
data/lib/sidekiq/job.rb CHANGED
@@ -1,13 +1,400 @@
1
- require "sidekiq/worker"
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/client"
2
4
 
3
5
  module Sidekiq
4
- # Sidekiq::Job is a new alias for Sidekiq::Worker as of Sidekiq 6.3.0.
5
- # Use `include Sidekiq::Job` rather than `include Sidekiq::Worker`.
6
- #
7
- # The term "worker" is too generic and overly confusing, used in several
8
- # different contexts meaning different things. Many people call a Sidekiq
9
- # process a "worker". Some people call the thread that executes jobs a
10
- # "worker". This change brings Sidekiq closer to ActiveJob where your job
11
- # classes extend ApplicationJob.
12
- Job = Worker
6
+ ##
7
+ # Include this module in your job class and you can easily create
8
+ # asynchronous jobs:
9
+ #
10
+ # class HardJob
11
+ # include Sidekiq::Job
12
+ # sidekiq_options queue: 'critical', retry: 5
13
+ #
14
+ # def perform(*args)
15
+ # # do some work
16
+ # end
17
+ # end
18
+ #
19
+ # Then in your Rails app, you can do this:
20
+ #
21
+ # HardJob.perform_async(1, 2, 3)
22
+ #
23
+ # Note that perform_async is a class method, perform is an instance method.
24
+ #
25
+ # Sidekiq::Job also includes several APIs to provide compatibility with
26
+ # ActiveJob.
27
+ #
28
+ # class SomeJob
29
+ # include Sidekiq::Job
30
+ # queue_as :critical
31
+ #
32
+ # def perform(...)
33
+ # end
34
+ # end
35
+ #
36
+ # SomeJob.set(wait_until: 1.hour).perform_async(123)
37
+ #
38
+ # Note that arguments passed to the job must still obey Sidekiq's
39
+ # best practice for simple, JSON-native data types. Sidekiq will not
40
+ # implement ActiveJob's more complex argument serialization. For
41
+ # this reason, we don't implement `perform_later` as our call semantics
42
+ # are very different.
43
+ #
44
+ module Job
45
+ ##
46
+ # The Options module is extracted so we can include it in ActiveJob::Base
47
+ # and allow native AJs to configure Sidekiq features/internals.
48
+ module Options
49
+ def self.included(base)
50
+ base.extend(ClassMethods)
51
+ base.sidekiq_class_attribute :sidekiq_options_hash
52
+ base.sidekiq_class_attribute :sidekiq_retry_in_block
53
+ base.sidekiq_class_attribute :sidekiq_retries_exhausted_block
54
+ end
55
+
56
+ module ClassMethods
57
+ ACCESSOR_MUTEX = Mutex.new
58
+
59
+ ##
60
+ # Allows customization for this type of Job.
61
+ # Legal options:
62
+ #
63
+ # queue - name of queue to use for this job type, default *default*
64
+ # retry - enable retries for this Job in case of error during execution,
65
+ # *true* to use the default or *Integer* count
66
+ # backtrace - whether to save any error backtrace in the retry payload to display in web UI,
67
+ # can be true, false or an integer number of lines to save, default *false*
68
+ #
69
+ # In practice, any option is allowed. This is the main mechanism to configure the
70
+ # options for a specific job.
71
+ def sidekiq_options(opts = {})
72
+ # stringify 2 levels of keys
73
+ opts = opts.to_h do |k, v|
74
+ [k.to_s, (Hash === v) ? v.transform_keys(&:to_s) : v]
75
+ end
76
+
77
+ self.sidekiq_options_hash = get_sidekiq_options.merge(opts)
78
+ end
79
+
80
+ def sidekiq_retry_in(&block)
81
+ self.sidekiq_retry_in_block = block
82
+ end
83
+
84
+ def sidekiq_retries_exhausted(&block)
85
+ self.sidekiq_retries_exhausted_block = block
86
+ end
87
+
88
+ def get_sidekiq_options # :nodoc:
89
+ self.sidekiq_options_hash ||= Sidekiq.default_job_options
90
+ end
91
+
92
+ def sidekiq_class_attribute(*attrs)
93
+ instance_reader = true
94
+ instance_writer = true
95
+
96
+ attrs.each do |name|
97
+ synchronized_getter = "__synchronized_#{name}"
98
+
99
+ singleton_class.instance_eval do
100
+ undef_method(name) if method_defined?(name) || private_method_defined?(name)
101
+ end
102
+
103
+ define_singleton_method(synchronized_getter) { nil }
104
+ singleton_class.class_eval do
105
+ private(synchronized_getter)
106
+ end
107
+
108
+ define_singleton_method(name) { ACCESSOR_MUTEX.synchronize { send synchronized_getter } }
109
+
110
+ ivar = "@#{name}"
111
+
112
+ singleton_class.instance_eval do
113
+ m = "#{name}="
114
+ undef_method(m) if method_defined?(m) || private_method_defined?(m)
115
+ end
116
+ define_singleton_method(:"#{name}=") do |val|
117
+ singleton_class.class_eval do
118
+ ACCESSOR_MUTEX.synchronize do
119
+ undef_method(synchronized_getter) if method_defined?(synchronized_getter) || private_method_defined?(synchronized_getter)
120
+ define_method(synchronized_getter) { val }
121
+ end
122
+ end
123
+
124
+ if singleton_class?
125
+ class_eval do
126
+ undef_method(name) if method_defined?(name) || private_method_defined?(name)
127
+ define_method(name) do
128
+ if instance_variable_defined? ivar
129
+ instance_variable_get ivar
130
+ else
131
+ singleton_class.send name
132
+ end
133
+ end
134
+ end
135
+ end
136
+ val
137
+ end
138
+
139
+ if instance_reader
140
+ undef_method(name) if method_defined?(name) || private_method_defined?(name)
141
+ define_method(name) do
142
+ if instance_variable_defined?(ivar)
143
+ instance_variable_get ivar
144
+ else
145
+ self.class.public_send name
146
+ end
147
+ end
148
+ end
149
+
150
+ if instance_writer
151
+ m = "#{name}="
152
+ undef_method(m) if method_defined?(m) || private_method_defined?(m)
153
+ attr_writer name
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ attr_accessor :jid
161
+
162
+ # This attribute is implementation-specific and not a public API
163
+ attr_accessor :_context
164
+
165
+ def self.included(base)
166
+ raise ArgumentError, "Sidekiq::Job cannot be included in an ActiveJob: #{base.name}" if base.ancestors.any? { |c| c.name == "ActiveJob::Base" }
167
+
168
+ base.include(Options)
169
+ base.extend(ClassMethods)
170
+ end
171
+
172
+ def logger
173
+ Sidekiq.logger
174
+ end
175
+
176
+ def interrupted?
177
+ @_context&.stopping?
178
+ end
179
+
180
+ # This helper class encapsulates the set options for `set`, e.g.
181
+ #
182
+ # SomeJob.set(queue: 'foo').perform_async(....)
183
+ #
184
+ class Setter
185
+ include Sidekiq::JobUtil
186
+
187
+ def initialize(klass, opts)
188
+ @klass = klass
189
+ # NB: the internal hash always has stringified keys
190
+ @opts = opts.transform_keys(&:to_s)
191
+
192
+ # ActiveJob compatibility
193
+ interval = @opts.delete("wait_until") || @opts.delete("wait")
194
+ at(interval) if interval
195
+ end
196
+
197
+ def set(options)
198
+ hash = options.transform_keys(&:to_s)
199
+ interval = hash.delete("wait_until") || hash.delete("wait")
200
+ @opts.merge!(hash)
201
+ at(interval) if interval
202
+ self
203
+ end
204
+
205
+ def perform_async(*args)
206
+ if @opts["sync"] == true
207
+ perform_inline(*args)
208
+ else
209
+ @klass.client_push(@opts.merge("args" => args, "class" => @klass))
210
+ end
211
+ end
212
+
213
+ # Explicit inline execution of a job. Returns nil if the job did not
214
+ # execute, true otherwise.
215
+ def perform_inline(*args)
216
+ raw = @opts.merge("args" => args, "class" => @klass)
217
+
218
+ # validate and normalize payload
219
+ item = normalize_item(raw)
220
+ queue = item["queue"]
221
+
222
+ # run client-side middleware
223
+ cfg = Sidekiq.default_configuration
224
+ result = cfg.client_middleware.invoke(item["class"], item, queue, cfg.redis_pool) do
225
+ item
226
+ end
227
+ return nil unless result
228
+
229
+ verify_json(item)
230
+
231
+ # round-trip the payload via JSON
232
+ msg = Sidekiq.load_json(Sidekiq.dump_json(item))
233
+
234
+ # prepare the job instance
235
+ klass = Object.const_get(msg["class"])
236
+ job = klass.new
237
+ job.jid = msg["jid"]
238
+ job.bid = msg["bid"] if job.respond_to?(:bid)
239
+
240
+ # run the job through server-side middleware
241
+ result = cfg.server_middleware.invoke(job, msg, msg["queue"]) do
242
+ # perform it
243
+ job.perform(*msg["args"])
244
+ true
245
+ end
246
+ return nil unless result
247
+ # jobs do not return a result. they should store any
248
+ # modified state.
249
+ true
250
+ end
251
+ alias_method :perform_sync, :perform_inline
252
+
253
+ def perform_bulk(args, **options)
254
+ client = @klass.build_client
255
+ client.push_bulk(@opts.merge({"class" => @klass, "args" => args}, options))
256
+ end
257
+
258
+ # +interval+ must be a timestamp, numeric or something that acts
259
+ # numeric (like an activesupport time interval).
260
+ def perform_in(interval, *args)
261
+ at(interval).perform_async(*args)
262
+ end
263
+ alias_method :perform_at, :perform_in
264
+
265
+ private
266
+
267
+ def at(interval)
268
+ int = interval.to_f
269
+ now = Time.now.to_f
270
+ ts = ((int < 1_000_000_000) ? now + int : int)
271
+ # Optimization to enqueue something now that is scheduled to go out now or in the past
272
+ @opts["at"] = ts if ts > now
273
+ self
274
+ end
275
+ end
276
+
277
+ module ClassMethods
278
+ def delay(*args)
279
+ raise ArgumentError, "Do not call .delay on a Sidekiq::Job class, call .perform_async"
280
+ end
281
+
282
+ def delay_for(*args)
283
+ raise ArgumentError, "Do not call .delay_for on a Sidekiq::Job class, call .perform_in"
284
+ end
285
+
286
+ def delay_until(*args)
287
+ raise ArgumentError, "Do not call .delay_until on a Sidekiq::Job class, call .perform_at"
288
+ end
289
+
290
+ def queue_as(q)
291
+ sidekiq_options("queue" => q.to_s)
292
+ end
293
+
294
+ def set(options)
295
+ Setter.new(self, options)
296
+ end
297
+
298
+ def perform_async(*args)
299
+ Setter.new(self, {}).perform_async(*args)
300
+ end
301
+
302
+ # Inline execution of job's perform method after passing through Sidekiq.client_middleware and Sidekiq.server_middleware
303
+ def perform_inline(*args)
304
+ Setter.new(self, {}).perform_inline(*args)
305
+ end
306
+ alias_method :perform_sync, :perform_inline
307
+
308
+ ##
309
+ # Push a large number of jobs to Redis, while limiting the batch of
310
+ # each job payload to 1,000. This method helps cut down on the number
311
+ # of round trips to Redis, which can increase the performance of enqueueing
312
+ # large numbers of jobs.
313
+ #
314
+ # +items+ must be an Array of Arrays.
315
+ #
316
+ # The +:at+ option schedules the jobs for future execution. It accepts
317
+ # either a single Numeric timestamp (or seconds-from-now) applied to every
318
+ # job, or an Array of Numeric values with the same size as +items+ to
319
+ # schedule each job at its corresponding time.
320
+ #
321
+ # For finer-grained control, use `Sidekiq::Client.push_bulk` directly.
322
+ #
323
+ # Example (3 Redis round trips):
324
+ #
325
+ # SomeJob.perform_async(1)
326
+ # SomeJob.perform_async(2)
327
+ # SomeJob.perform_async(3)
328
+ #
329
+ # Would instead become (1 Redis round trip):
330
+ #
331
+ # SomeJob.perform_bulk([[1], [2], [3]])
332
+ #
333
+ # Scheduling every job 60 seconds from now (single Numeric +:at+):
334
+ #
335
+ # SomeJob.perform_bulk([[1], [2], [3]], at: 60)
336
+ #
337
+ # Scheduling each job at its own time (Array +:at+):
338
+ #
339
+ # SomeJob.perform_bulk([[1], [2]], at: [Time.now.to_f + 30, Time.now.to_f + 60])
340
+ #
341
+ def perform_bulk(*args, **kwargs)
342
+ Setter.new(self, {}).perform_bulk(*args, **kwargs)
343
+ end
344
+
345
+ # +interval+ must be a timestamp, numeric or something that acts
346
+ # numeric (like an activesupport time interval).
347
+ def perform_in(interval, *args)
348
+ int = interval.to_f
349
+ now = Time.now.to_f
350
+ ts = ((int < 1_000_000_000) ? now + int : int)
351
+
352
+ item = {"class" => self, "args" => args}
353
+
354
+ # Optimization to enqueue something now that is scheduled to go out now or in the past
355
+ item["at"] = ts if ts > now
356
+
357
+ client_push(item)
358
+ end
359
+ alias_method :perform_at, :perform_in
360
+
361
+ ##
362
+ # Allows customization for this type of Job.
363
+ # Legal options:
364
+ #
365
+ # queue - use a named queue for this Job, default 'default'
366
+ # retry - enable the RetryJobs middleware for this Job, *true* to use the default
367
+ # or *Integer* count
368
+ # backtrace - whether to save any error backtrace in the retry payload to display in web UI,
369
+ # can be true, false or an integer number of lines to save, default *false*
370
+ # pool - use the given Redis connection pool to push this type of job to a given shard.
371
+ #
372
+ # In practice, any option is allowed. This is the main mechanism to configure the
373
+ # options for a specific job.
374
+ def sidekiq_options(opts = {})
375
+ super
376
+ end
377
+
378
+ def client_push(item) # :nodoc:
379
+ raise ArgumentError, "Job payloads should contain no Symbols: #{item}" if item.any? { |k, v| k.is_a?(::Symbol) }
380
+
381
+ # allow the user to dynamically re-target jobs to another shard using the "pool" attribute
382
+ # FooJob.set(pool: SOME_POOL).perform_async
383
+ old = Thread.current[:sidekiq_redis_pool]
384
+ pool = item.delete("pool")
385
+ Thread.current[:sidekiq_redis_pool] = pool if pool
386
+ begin
387
+ build_client.push(item)
388
+ ensure
389
+ Thread.current[:sidekiq_redis_pool] = old
390
+ end
391
+ end
392
+
393
+ def build_client # :nodoc:
394
+ pool = Thread.current[:sidekiq_redis_pool] || get_sidekiq_options["pool"] || Sidekiq.default_configuration.redis_pool
395
+ client_class = Thread.current[:sidekiq_client_class] || get_sidekiq_options["client_class"] || Sidekiq::Client
396
+ client_class.new(pool: pool)
397
+ end
398
+ end
399
+ end
13
400
  end
@@ -2,22 +2,23 @@
2
2
 
3
3
  module Sidekiq
4
4
  class JobLogger
5
- def initialize(logger = Sidekiq.logger)
6
- @logger = logger
5
+ def initialize(config)
6
+ @config = config
7
+ @logger = @config.logger
8
+ @skip = !!@config[:skip_default_job_logging]
7
9
  end
8
10
 
9
11
  def call(item, queue)
10
12
  start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
11
- @logger.info("start")
13
+ @logger.info { "start" } unless @skip
12
14
 
13
15
  yield
14
16
 
15
17
  Sidekiq::Context.add(:elapsed, elapsed(start))
16
- @logger.info("done")
18
+ @logger.info { "done" } unless @skip
17
19
  rescue Exception
18
20
  Sidekiq::Context.add(:elapsed, elapsed(start))
19
- @logger.info("fail")
20
-
21
+ @logger.info { "fail" } unless @skip
21
22
  raise
22
23
  end
23
24
 
@@ -25,16 +26,18 @@ module Sidekiq
25
26
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
26
27
  # attribute to expose the underlying thing.
27
28
  h = {
28
- class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
29
- jid: job_hash["jid"]
29
+ jid: job_hash["jid"],
30
+ class: job_hash["wrapped"] || job_hash["class"]
30
31
  }
31
- h[:bid] = job_hash["bid"] if job_hash.has_key?("bid")
32
- h[:tags] = job_hash["tags"] if job_hash.has_key?("tags")
32
+
33
+ @config[:logged_job_attributes].each do |attr|
34
+ h[attr.to_sym] = job_hash[attr] if job_hash.has_key?(attr)
35
+ end
33
36
 
34
37
  Thread.current[:sidekiq_context] = h
35
38
  level = job_hash["log_level"]
36
39
  if level
37
- @logger.log_at(level, &block)
40
+ @logger.with_level(level, &block)
38
41
  else
39
42
  yield
40
43
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zlib"
4
- require "base64"
5
4
  require "sidekiq/component"
6
5
 
7
6
  module Sidekiq
@@ -49,7 +48,7 @@ module Sidekiq
49
48
  # The default number of retries is 25 which works out to about 3 weeks
50
49
  # You can change the default maximum number of retries in your initializer:
51
50
  #
52
- # Sidekiq.options[:max_retries] = 7
51
+ # Sidekiq.default_configuration[:max_retries] = 7
53
52
  #
54
53
  # or limit the number of retries for a particular job and send retries to
55
54
  # a low priority queue with:
@@ -60,17 +59,23 @@ module Sidekiq
60
59
  # end
61
60
  #
62
61
  class JobRetry
62
+ # Handled means the job failed but has been dealt with
63
+ # (by creating a retry, rescheduling it, etc). It still
64
+ # needs to be logged and dispatched to error_handlers.
63
65
  class Handled < ::RuntimeError; end
64
66
 
67
+ # Skip means the job failed but Sidekiq does not need to
68
+ # create a retry, log it or send to error_handlers.
65
69
  class Skip < Handled; end
66
70
 
67
71
  include Sidekiq::Component
68
72
 
69
73
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
70
74
 
71
- def initialize(options)
72
- @config = options
73
- @max_retries = @config[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
75
+ def initialize(cfg)
76
+ @config = cfg
77
+ @max_retries = Sidekiq.default_configuration[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
78
+ @backtrace_cleaner = Sidekiq.default_configuration[:backtrace_cleaner]
74
79
  end
75
80
 
76
81
  # The global retry handler requires only the barest of data.
@@ -91,7 +96,7 @@ module Sidekiq
91
96
  if msg["retry"]
92
97
  process_retry(nil, msg, queue, e)
93
98
  else
94
- Sidekiq.death_handlers.each do |handler|
99
+ @config[:death_handlers].each do |handler|
95
100
  handler.call(msg, e)
96
101
  rescue => handler_ex
97
102
  handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
@@ -129,11 +134,15 @@ module Sidekiq
129
134
  process_retry(jobinst, msg, queue, e)
130
135
  # We've handled this error associated with this job, don't
131
136
  # need to handle it at the global level
132
- raise Skip
137
+ raise Handled
133
138
  end
134
139
 
135
140
  private
136
141
 
142
+ def now_ms
143
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
144
+ end
145
+
137
146
  # Note that +jobinst+ can be nil here if an error is raised before we can
138
147
  # instantiate the job instance. All access must be guarded and
139
148
  # best effort.
@@ -144,44 +153,53 @@ module Sidekiq
144
153
 
145
154
  m = exception_message(exception)
146
155
  if m.respond_to?(:scrub!)
147
- m.force_encoding("utf-8")
156
+ m.force_encoding(Encoding::UTF_8)
148
157
  m.scrub!
149
158
  end
150
159
 
151
160
  msg["error_message"] = m
152
161
  msg["error_class"] = exception.class.name
153
162
  count = if msg["retry_count"]
154
- msg["retried_at"] = Time.now.to_f
163
+ msg["retried_at"] = now_ms
155
164
  msg["retry_count"] += 1
156
165
  else
157
- msg["failed_at"] = Time.now.to_f
166
+ msg["failed_at"] = now_ms
158
167
  msg["retry_count"] = 0
159
168
  end
160
169
 
161
170
  if msg["backtrace"]
171
+ backtrace = @backtrace_cleaner.call(exception.backtrace)
162
172
  lines = if msg["backtrace"] == true
163
- exception.backtrace
173
+ backtrace
164
174
  else
165
- exception.backtrace[0...msg["backtrace"].to_i]
175
+ backtrace[0...msg["backtrace"].to_i]
166
176
  end
167
177
 
168
178
  msg["error_backtrace"] = compress_backtrace(lines)
169
179
  end
170
180
 
171
- # Goodbye dear message, you (re)tried your best I'm sure.
172
- return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
181
+ # retry_for and retry are mutually exclusive - if retry_for is set,
182
+ # we exclusively use duration-based retry logic and ignore count-based logic
183
+ rf = msg["retry_for"]
184
+ if rf
185
+ return retries_exhausted(jobinst, msg, exception) if (time_for(msg["failed_at"]) + rf) < Time.now
186
+ elsif count >= max_retry_attempts
187
+ return retries_exhausted(jobinst, msg, exception)
188
+ end
173
189
 
174
- strategy, delay = delay_for(jobinst, count, exception)
190
+ strategy, delay = delay_for(jobinst, count, exception, msg)
175
191
  case strategy
176
192
  when :discard
177
- return # poof!
193
+ msg["discarded_at"] = now_ms
194
+
195
+ return run_death_handlers(msg, exception)
178
196
  when :kill
179
197
  return retries_exhausted(jobinst, msg, exception)
180
198
  end
181
199
 
182
200
  # Logging here can break retries if the logging device raises ENOSPC #3979
183
201
  # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
184
- jitter = rand(10) * (count + 1)
202
+ jitter = rand(10 * (count + 1))
185
203
  retry_at = Time.now.to_f + delay + jitter
186
204
  payload = Sidekiq.dump_json(msg)
187
205
  redis do |conn|
@@ -189,18 +207,34 @@ module Sidekiq
189
207
  end
190
208
  end
191
209
 
210
+ def time_for(item)
211
+ if item.is_a?(Float)
212
+ Time.at(item)
213
+ else
214
+ Time.at(item / 1000, item % 1000, :millisecond)
215
+ end
216
+ end
217
+
192
218
  # returns (strategy, seconds)
193
- def delay_for(jobinst, count, exception)
219
+ def delay_for(jobinst, count, exception, msg)
194
220
  rv = begin
195
221
  # sidekiq_retry_in can return two different things:
196
222
  # 1. When to retry next, as an integer of seconds
197
223
  # 2. A symbol which re-routes the job elsewhere, e.g. :discard, :kill, :default
198
- jobinst&.sidekiq_retry_in_block&.call(count, exception)
224
+ block = jobinst&.sidekiq_retry_in_block
225
+
226
+ # the sidekiq_retry_in_block can be defined in a wrapped class (ActiveJob for instance)
227
+ unless msg["wrapped"].nil?
228
+ wrapped = Object.const_get(msg["wrapped"])
229
+ block = wrapped.respond_to?(:sidekiq_retry_in_block) ? wrapped.sidekiq_retry_in_block : nil
230
+ end
231
+ block&.call(count, exception, msg)
199
232
  rescue Exception => e
200
233
  handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
201
234
  nil
202
235
  end
203
236
 
237
+ rv = rv.to_i if rv.respond_to?(:to_i)
204
238
  delay = (count**4) + 15
205
239
  if Integer === rv && rv > 0
206
240
  delay = rv
@@ -214,19 +248,35 @@ module Sidekiq
214
248
  end
215
249
 
216
250
  def retries_exhausted(jobinst, msg, exception)
217
- begin
251
+ rv = begin
218
252
  block = jobinst&.sidekiq_retries_exhausted_block
253
+
254
+ # the sidekiq_retries_exhausted_block can be defined in a wrapped class (ActiveJob for instance)
255
+ unless msg["wrapped"].nil?
256
+ wrapped = Object.const_get(msg["wrapped"])
257
+ block = wrapped.respond_to?(:sidekiq_retries_exhausted_block) ? wrapped.sidekiq_retries_exhausted_block : nil
258
+ end
219
259
  block&.call(msg, exception)
220
260
  rescue => e
221
261
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
222
262
  end
223
263
 
224
- send_to_morgue(msg) unless msg["dead"] == false
264
+ discarded = msg["dead"] == false || rv == :discard
265
+
266
+ if discarded
267
+ msg["discarded_at"] = now_ms
268
+ else
269
+ send_to_morgue(msg)
270
+ end
271
+
272
+ run_death_handlers(msg, exception)
273
+ end
225
274
 
226
- config.death_handlers.each do |handler|
227
- handler.call(msg, exception)
275
+ def run_death_handlers(job, exception)
276
+ @config[:death_handlers].each do |handler|
277
+ handler.call(job, exception)
228
278
  rescue => e
229
- handle_exception(e, {context: "Error calling death handler", job: msg})
279
+ handle_exception(e, {context: "Error calling death handler", job: job})
230
280
  end
231
281
  end
232
282
 
@@ -235,11 +285,11 @@ module Sidekiq
235
285
  payload = Sidekiq.dump_json(msg)
236
286
  now = Time.now.to_f
237
287
 
238
- config.redis do |conn|
288
+ redis do |conn|
239
289
  conn.multi do |xa|
240
290
  xa.zadd("dead", now.to_s, payload)
241
- xa.zremrangebyscore("dead", "-inf", now - config[:dead_timeout_in_seconds])
242
- xa.zremrangebyrank("dead", 0, - config[:dead_max_jobs])
291
+ xa.zremrangebyscore("dead", "-inf", now - @config[:dead_timeout_in_seconds])
292
+ xa.zremrangebyrank("dead", 0, - @config[:dead_max_jobs])
243
293
  end
244
294
  end
245
295
  end
@@ -276,7 +326,7 @@ module Sidekiq
276
326
  def compress_backtrace(backtrace)
277
327
  serialized = Sidekiq.dump_json(backtrace)
278
328
  compressed = Zlib::Deflate.deflate(serialized)
279
- Base64.encode64(compressed)
329
+ [compressed].pack("m0") # Base64.strict_encode64
280
330
  end
281
331
  end
282
332
  end