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,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/scheduled"
4
- require "sidekiq/api"
5
-
6
3
  require "zlib"
7
- require "base64"
4
+ require "sidekiq/component"
8
5
 
9
6
  module Sidekiq
10
7
  ##
@@ -25,18 +22,19 @@ module Sidekiq
25
22
  #
26
23
  # A job looks like:
27
24
  #
28
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
25
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
29
26
  #
30
27
  # The 'retry' option also accepts a number (in place of 'true'):
31
28
  #
32
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
29
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
33
30
  #
34
31
  # The job will be retried this number of times before giving up. (If simply
35
32
  # 'true', Sidekiq retries 25 times)
36
33
  #
37
- # We'll add a bit more data to the job to support retries:
34
+ # Relevant options for job retries:
38
35
  #
39
- # * 'queue' - the queue to use
36
+ # * 'queue' - the queue for the initial job
37
+ # * 'retry_queue' - if job retries should be pushed to a different (e.g. lower priority) queue
40
38
  # * 'retry_count' - number of times we've retried so far.
41
39
  # * 'error_message' - the message from the exception
42
40
  # * 'error_class' - the exception class
@@ -50,31 +48,39 @@ module Sidekiq
50
48
  # The default number of retries is 25 which works out to about 3 weeks
51
49
  # You can change the default maximum number of retries in your initializer:
52
50
  #
53
- # Sidekiq.options[:max_retries] = 7
51
+ # Sidekiq.default_configuration[:max_retries] = 7
54
52
  #
55
- # or limit the number of retries for a particular worker with:
53
+ # or limit the number of retries for a particular job and send retries to
54
+ # a low priority queue with:
56
55
  #
57
- # class MyWorker
58
- # include Sidekiq::Worker
59
- # sidekiq_options :retry => 10
56
+ # class MyJob
57
+ # include Sidekiq::Job
58
+ # sidekiq_options retry: 10, retry_queue: 'low'
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
- include Sidekiq::Util
71
+ include Sidekiq::Component
68
72
 
69
73
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
70
74
 
71
- def initialize(options = {})
72
- @max_retries = Sidekiq.options.merge(options).fetch(:max_retries, DEFAULT_MAX_RETRY_ATTEMPTS)
75
+ def initialize(capsule)
76
+ @config = @capsule = capsule
77
+ @max_retries = Sidekiq.default_configuration[:max_retries] || DEFAULT_MAX_RETRY_ATTEMPTS
78
+ @backtrace_cleaner = Sidekiq.default_configuration[:backtrace_cleaner]
73
79
  end
74
80
 
75
81
  # The global retry handler requires only the barest of data.
76
82
  # We want to be able to retry as much as possible so we don't
77
- # require the worker to be instantiated.
83
+ # require the job to be instantiated.
78
84
  def global(jobstr, queue)
79
85
  yield
80
86
  rescue Handled => ex
@@ -88,9 +94,9 @@ module Sidekiq
88
94
 
89
95
  msg = Sidekiq.load_json(jobstr)
90
96
  if msg["retry"]
91
- attempt_retry(nil, msg, queue, e)
97
+ process_retry(nil, msg, queue, e)
92
98
  else
93
- Sidekiq.death_handlers.each do |handler|
99
+ @capsule.config.death_handlers.each do |handler|
94
100
  handler.call(msg, e)
95
101
  rescue => handler_ex
96
102
  handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
@@ -101,14 +107,14 @@ module Sidekiq
101
107
  end
102
108
 
103
109
  # The local retry support means that any errors that occur within
104
- # this block can be associated with the given worker instance.
110
+ # this block can be associated with the given job instance.
105
111
  # This is required to support the `sidekiq_retries_exhausted` block.
106
112
  #
107
113
  # Note that any exception from the block is wrapped in the Skip
108
114
  # exception so the global block does not reprocess the error. The
109
115
  # Skip exception is unwrapped within Sidekiq::Processor#process before
110
116
  # calling the handle_exception handlers.
111
- def local(worker, jobstr, queue)
117
+ def local(jobinst, jobstr, queue)
112
118
  yield
113
119
  rescue Handled => ex
114
120
  raise ex
@@ -121,88 +127,171 @@ module Sidekiq
121
127
 
122
128
  msg = Sidekiq.load_json(jobstr)
123
129
  if msg["retry"].nil?
124
- msg["retry"] = worker.class.get_sidekiq_options["retry"]
130
+ msg["retry"] = jobinst.class.get_sidekiq_options["retry"]
125
131
  end
126
132
 
127
133
  raise e unless msg["retry"]
128
- attempt_retry(worker, msg, queue, e)
134
+ process_retry(jobinst, msg, queue, e)
129
135
  # We've handled this error associated with this job, don't
130
136
  # need to handle it at the global level
131
- raise Skip
137
+ raise Handled
132
138
  end
133
139
 
134
140
  private
135
141
 
136
- # Note that +worker+ can be nil here if an error is raised before we can
137
- # instantiate the worker instance. All access must be guarded and
142
+ def now_ms
143
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
144
+ end
145
+
146
+ # Note that +jobinst+ can be nil here if an error is raised before we can
147
+ # instantiate the job instance. All access must be guarded and
138
148
  # best effort.
139
- def attempt_retry(worker, msg, queue, exception)
149
+ def process_retry(jobinst, msg, queue, exception)
140
150
  max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
141
151
 
142
152
  msg["queue"] = (msg["retry_queue"] || queue)
143
153
 
144
154
  m = exception_message(exception)
145
155
  if m.respond_to?(:scrub!)
146
- m.force_encoding("utf-8")
156
+ m.force_encoding(Encoding::UTF_8)
147
157
  m.scrub!
148
158
  end
149
159
 
150
160
  msg["error_message"] = m
151
161
  msg["error_class"] = exception.class.name
152
162
  count = if msg["retry_count"]
153
- msg["retried_at"] = Time.now.to_f
163
+ msg["retried_at"] = now_ms
154
164
  msg["retry_count"] += 1
155
165
  else
156
- msg["failed_at"] = Time.now.to_f
166
+ msg["failed_at"] = now_ms
157
167
  msg["retry_count"] = 0
158
168
  end
159
169
 
160
170
  if msg["backtrace"]
171
+ backtrace = @backtrace_cleaner.call(exception.backtrace)
161
172
  lines = if msg["backtrace"] == true
162
- exception.backtrace
173
+ backtrace
163
174
  else
164
- exception.backtrace[0...msg["backtrace"].to_i]
175
+ backtrace[0...msg["backtrace"].to_i]
165
176
  end
166
177
 
167
178
  msg["error_backtrace"] = compress_backtrace(lines)
168
179
  end
169
180
 
170
- if count < max_retry_attempts
171
- delay = delay_for(worker, count, exception)
172
- # Logging here can break retries if the logging device raises ENOSPC #3979
173
- # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
174
- retry_at = Time.now.to_f + delay
175
- payload = Sidekiq.dump_json(msg)
176
- Sidekiq.redis do |conn|
177
- conn.zadd("retry", retry_at.to_s, payload)
178
- end
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
189
+
190
+ strategy, delay = delay_for(jobinst, count, exception, msg)
191
+ case strategy
192
+ when :discard
193
+ msg["discarded_at"] = now_ms
194
+
195
+ return run_death_handlers(msg, exception)
196
+ when :kill
197
+ return retries_exhausted(jobinst, msg, exception)
198
+ end
199
+
200
+ # Logging here can break retries if the logging device raises ENOSPC #3979
201
+ # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
202
+ jitter = rand(10 * (count + 1))
203
+ retry_at = Time.now.to_f + delay + jitter
204
+ payload = Sidekiq.dump_json(msg)
205
+ redis do |conn|
206
+ conn.zadd("retry", retry_at.to_s, payload)
207
+ end
208
+ end
209
+
210
+ def time_for(item)
211
+ if item.is_a?(Float)
212
+ Time.at(item)
179
213
  else
180
- # Goodbye dear message, you (re)tried your best I'm sure.
181
- retries_exhausted(worker, msg, exception)
214
+ Time.at(item / 1000, item % 1000)
182
215
  end
183
216
  end
184
217
 
185
- def retries_exhausted(worker, msg, exception)
186
- begin
187
- block = worker&.sidekiq_retries_exhausted_block
218
+ # returns (strategy, seconds)
219
+ def delay_for(jobinst, count, exception, msg)
220
+ rv = begin
221
+ # sidekiq_retry_in can return two different things:
222
+ # 1. When to retry next, as an integer of seconds
223
+ # 2. A symbol which re-routes the job elsewhere, e.g. :discard, :kill, :default
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)
232
+ rescue Exception => e
233
+ handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
234
+ nil
235
+ end
236
+
237
+ rv = rv.to_i if rv.respond_to?(:to_i)
238
+ delay = (count**4) + 15
239
+ if Integer === rv && rv > 0
240
+ delay = rv
241
+ elsif rv == :discard
242
+ return [:discard, nil] # do nothing, job goes poof
243
+ elsif rv == :kill
244
+ return [:kill, nil]
245
+ end
246
+
247
+ [:default, delay]
248
+ end
249
+
250
+ def retries_exhausted(jobinst, msg, exception)
251
+ rv = begin
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
188
259
  block&.call(msg, exception)
189
260
  rescue => e
190
261
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
191
262
  end
192
263
 
193
- 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
194
274
 
195
- Sidekiq.death_handlers.each do |handler|
196
- handler.call(msg, exception)
275
+ def run_death_handlers(job, exception)
276
+ @capsule.config.death_handlers.each do |handler|
277
+ handler.call(job, exception)
197
278
  rescue => e
198
- handle_exception(e, {context: "Error calling death handler", job: msg})
279
+ handle_exception(e, {context: "Error calling death handler", job: job})
199
280
  end
200
281
  end
201
282
 
202
283
  def send_to_morgue(msg)
203
284
  logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
204
285
  payload = Sidekiq.dump_json(msg)
205
- DeadSet.new.kill(payload, notify_failure: false)
286
+ now = Time.now.to_f
287
+
288
+ redis do |conn|
289
+ conn.multi do |xa|
290
+ xa.zadd("dead", now.to_s, payload)
291
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
292
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
293
+ end
294
+ end
206
295
  end
207
296
 
208
297
  def retry_attempts_from(msg_retry, default)
@@ -213,22 +302,6 @@ module Sidekiq
213
302
  end
214
303
  end
215
304
 
216
- def delay_for(worker, count, exception)
217
- jitter = rand(10) * (count + 1)
218
- if worker&.sidekiq_retry_in_block
219
- custom_retry_in = retry_in(worker, count, exception).to_i
220
- return custom_retry_in + jitter if custom_retry_in > 0
221
- end
222
- (count**4) + 15 + jitter
223
- end
224
-
225
- def retry_in(worker, count, exception)
226
- worker.sidekiq_retry_in_block.call(count, exception)
227
- rescue Exception => e
228
- handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"})
229
- nil
230
- end
231
-
232
305
  def exception_caused_by_shutdown?(e, checked_causes = [])
233
306
  return false unless e.cause
234
307
 
@@ -253,7 +326,7 @@ module Sidekiq
253
326
  def compress_backtrace(backtrace)
254
327
  serialized = Sidekiq.dump_json(backtrace)
255
328
  compressed = Zlib::Deflate.deflate(serialized)
256
- Base64.encode64(compressed)
329
+ [compressed].pack("m0") # Base64.strict_encode64
257
330
  end
258
331
  end
259
332
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module Sidekiq
7
+ module JobUtil
8
+ # These functions encapsulate various job utilities.
9
+
10
+ TRANSIENT_ATTRIBUTES = %w[]
11
+
12
+ def validate(item)
13
+ raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
14
+ raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array) || item["args"].is_a?(Enumerator::Lazy)
15
+ raise(ArgumentError, "Job class must be either a Class or String representation of the class name: `#{item}`") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
16
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
17
+ raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
18
+ raise(ArgumentError, "retry_for must be a relative amount of time, e.g. 48.hours `#{item}`") if item["retry_for"] && item["retry_for"] > 1_000_000_000
19
+ end
20
+
21
+ def verify_json(item)
22
+ job_class = item["wrapped"] || item["class"]
23
+ args = item["args"]
24
+ mode = Sidekiq::Config::DEFAULTS[:on_complex_arguments]
25
+
26
+ if mode == :raise || mode == :warn
27
+ if (unsafe_item = json_unsafe?(args))
28
+ msg = <<~EOM
29
+ Job arguments to #{job_class} must be native JSON types, but #{unsafe_item.inspect} is a #{unsafe_item.class}.
30
+ See https://github.com/sidekiq/sidekiq/wiki/Best-Practices
31
+ To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
32
+ EOM
33
+
34
+ if mode == :raise
35
+ raise(ArgumentError, msg)
36
+ else
37
+ warn(msg)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def normalize_item(item)
44
+ validate(item)
45
+
46
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
47
+ # this allows ActiveJobs to control sidekiq_options too.
48
+ defaults = normalized_hash(item["class"])
49
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?(:get_sidekiq_options)
50
+ item = defaults.merge(item)
51
+
52
+ raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
53
+
54
+ # remove job attributes which aren't necessary to persist into Redis
55
+ TRANSIENT_ATTRIBUTES.each { |key| item.delete(key) }
56
+
57
+ item["jid"] ||= SecureRandom.hex(12)
58
+ item["class"] = item["class"].to_s
59
+ item["queue"] = item["queue"].to_s
60
+ item["retry_for"] = item["retry_for"].to_i if item["retry_for"]
61
+ item["created_at"] ||= now_in_millis
62
+ item
63
+ end
64
+
65
+ def now_in_millis
66
+ ::Process.clock_gettime(::Process::CLOCK_REALTIME, :millisecond)
67
+ end
68
+
69
+ def normalized_hash(item_class)
70
+ if item_class.is_a?(Class)
71
+ raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
72
+ item_class.get_sidekiq_options
73
+ else
74
+ Sidekiq.default_job_options
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ RECURSIVE_JSON_UNSAFE = {
81
+ Integer => ->(val) {},
82
+ Float => ->(val) {},
83
+ TrueClass => ->(val) {},
84
+ FalseClass => ->(val) {},
85
+ NilClass => ->(val) {},
86
+ String => ->(val) {},
87
+ Array => ->(val) {
88
+ val.each do |e|
89
+ unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
90
+ return unsafe_item unless unsafe_item.nil?
91
+ end
92
+ nil
93
+ },
94
+ Hash => ->(val) {
95
+ val.each do |k, v|
96
+ return k unless String === k
97
+
98
+ unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
99
+ return unsafe_item unless unsafe_item.nil?
100
+ end
101
+ nil
102
+ }
103
+ }
104
+
105
+ RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
106
+ RECURSIVE_JSON_UNSAFE.compare_by_identity
107
+ private_constant :RECURSIVE_JSON_UNSAFE
108
+
109
+ def json_unsafe?(item)
110
+ RECURSIVE_JSON_UNSAFE[item.class].call(item)
111
+ end
112
+ end
113
+ end