sidekiq 5.2.7 → 8.0.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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +845 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +54 -54
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +219 -112
  8. data/bin/sidekiqmon +11 -0
  9. data/bin/webload +69 -0
  10. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +120 -0
  11. data/lib/generators/sidekiq/job_generator.rb +59 -0
  12. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  13. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  14. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  15. data/lib/sidekiq/api.rb +757 -373
  16. data/lib/sidekiq/capsule.rb +132 -0
  17. data/lib/sidekiq/cli.rb +210 -233
  18. data/lib/sidekiq/client.rb +145 -103
  19. data/lib/sidekiq/component.rb +128 -0
  20. data/lib/sidekiq/config.rb +315 -0
  21. data/lib/sidekiq/deploy.rb +64 -0
  22. data/lib/sidekiq/embedded.rb +64 -0
  23. data/lib/sidekiq/fetch.rb +49 -42
  24. data/lib/sidekiq/iterable_job.rb +56 -0
  25. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  26. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  27. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  28. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  29. data/lib/sidekiq/job/iterable.rb +306 -0
  30. data/lib/sidekiq/job.rb +385 -0
  31. data/lib/sidekiq/job_logger.rb +34 -7
  32. data/lib/sidekiq/job_retry.rb +164 -109
  33. data/lib/sidekiq/job_util.rb +113 -0
  34. data/lib/sidekiq/launcher.rb +208 -107
  35. data/lib/sidekiq/logger.rb +80 -0
  36. data/lib/sidekiq/manager.rb +42 -46
  37. data/lib/sidekiq/metrics/query.rb +184 -0
  38. data/lib/sidekiq/metrics/shared.rb +109 -0
  39. data/lib/sidekiq/metrics/tracking.rb +150 -0
  40. data/lib/sidekiq/middleware/chain.rb +113 -56
  41. data/lib/sidekiq/middleware/current_attributes.rb +119 -0
  42. data/lib/sidekiq/middleware/i18n.rb +7 -7
  43. data/lib/sidekiq/middleware/modules.rb +23 -0
  44. data/lib/sidekiq/monitor.rb +147 -0
  45. data/lib/sidekiq/paginator.rb +41 -16
  46. data/lib/sidekiq/processor.rb +146 -127
  47. data/lib/sidekiq/profiler.rb +72 -0
  48. data/lib/sidekiq/rails.rb +46 -43
  49. data/lib/sidekiq/redis_client_adapter.rb +113 -0
  50. data/lib/sidekiq/redis_connection.rb +79 -108
  51. data/lib/sidekiq/ring_buffer.rb +31 -0
  52. data/lib/sidekiq/scheduled.rb +112 -50
  53. data/lib/sidekiq/sd_notify.rb +149 -0
  54. data/lib/sidekiq/systemd.rb +26 -0
  55. data/lib/sidekiq/testing/inline.rb +6 -5
  56. data/lib/sidekiq/testing.rb +91 -90
  57. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  58. data/lib/sidekiq/version.rb +7 -1
  59. data/lib/sidekiq/web/action.rb +125 -60
  60. data/lib/sidekiq/web/application.rb +363 -259
  61. data/lib/sidekiq/web/config.rb +120 -0
  62. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  63. data/lib/sidekiq/web/helpers.rb +241 -120
  64. data/lib/sidekiq/web/router.rb +62 -71
  65. data/lib/sidekiq/web.rb +69 -161
  66. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  67. data/lib/sidekiq.rb +94 -182
  68. data/sidekiq.gemspec +26 -16
  69. data/web/assets/images/apple-touch-icon.png +0 -0
  70. data/web/assets/javascripts/application.js +150 -61
  71. data/web/assets/javascripts/base-charts.js +120 -0
  72. data/web/assets/javascripts/chart.min.js +13 -0
  73. data/web/assets/javascripts/chartjs-adapter-date-fns.min.js +7 -0
  74. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  75. data/web/assets/javascripts/dashboard-charts.js +194 -0
  76. data/web/assets/javascripts/dashboard.js +41 -293
  77. data/web/assets/javascripts/metrics.js +280 -0
  78. data/web/assets/stylesheets/style.css +766 -0
  79. data/web/locales/ar.yml +72 -65
  80. data/web/locales/cs.yml +63 -62
  81. data/web/locales/da.yml +61 -53
  82. data/web/locales/de.yml +66 -53
  83. data/web/locales/el.yml +44 -24
  84. data/web/locales/en.yml +94 -66
  85. data/web/locales/es.yml +92 -54
  86. data/web/locales/fa.yml +66 -65
  87. data/web/locales/fr.yml +83 -62
  88. data/web/locales/gd.yml +99 -0
  89. data/web/locales/he.yml +66 -64
  90. data/web/locales/hi.yml +60 -59
  91. data/web/locales/it.yml +93 -54
  92. data/web/locales/ja.yml +75 -64
  93. data/web/locales/ko.yml +53 -52
  94. data/web/locales/lt.yml +84 -0
  95. data/web/locales/nb.yml +62 -61
  96. data/web/locales/nl.yml +53 -52
  97. data/web/locales/pl.yml +46 -45
  98. data/web/locales/{pt-br.yml → pt-BR.yml} +84 -56
  99. data/web/locales/pt.yml +52 -51
  100. data/web/locales/ru.yml +69 -63
  101. data/web/locales/sv.yml +54 -53
  102. data/web/locales/ta.yml +61 -60
  103. data/web/locales/tr.yml +101 -0
  104. data/web/locales/uk.yml +86 -61
  105. data/web/locales/ur.yml +65 -64
  106. data/web/locales/vi.yml +84 -0
  107. data/web/locales/zh-CN.yml +106 -0
  108. data/web/locales/{zh-tw.yml → zh-TW.yml} +43 -9
  109. data/web/views/_footer.erb +31 -19
  110. data/web/views/_job_info.erb +94 -75
  111. data/web/views/_metrics_period_select.erb +15 -0
  112. data/web/views/_nav.erb +14 -21
  113. data/web/views/_paging.erb +23 -19
  114. data/web/views/_poll_link.erb +3 -6
  115. data/web/views/_summary.erb +23 -23
  116. data/web/views/busy.erb +139 -87
  117. data/web/views/dashboard.erb +82 -53
  118. data/web/views/dead.erb +31 -27
  119. data/web/views/filtering.erb +6 -0
  120. data/web/views/layout.erb +15 -29
  121. data/web/views/metrics.erb +84 -0
  122. data/web/views/metrics_for_job.erb +58 -0
  123. data/web/views/morgue.erb +60 -70
  124. data/web/views/profiles.erb +43 -0
  125. data/web/views/queue.erb +50 -39
  126. data/web/views/queues.erb +45 -29
  127. data/web/views/retries.erb +65 -75
  128. data/web/views/retry.erb +32 -27
  129. data/web/views/scheduled.erb +58 -52
  130. data/web/views/scheduled_job_info.erb +1 -1
  131. metadata +96 -76
  132. data/.circleci/config.yml +0 -61
  133. data/.github/contributing.md +0 -32
  134. data/.github/issue_template.md +0 -11
  135. data/.gitignore +0 -15
  136. data/.travis.yml +0 -11
  137. data/3.0-Upgrade.md +0 -70
  138. data/4.0-Upgrade.md +0 -53
  139. data/5.0-Upgrade.md +0 -56
  140. data/COMM-LICENSE +0 -97
  141. data/Ent-Changes.md +0 -238
  142. data/Gemfile +0 -23
  143. data/LICENSE +0 -9
  144. data/Pro-2.0-Upgrade.md +0 -138
  145. data/Pro-3.0-Upgrade.md +0 -44
  146. data/Pro-4.0-Upgrade.md +0 -35
  147. data/Pro-Changes.md +0 -759
  148. data/Rakefile +0 -9
  149. data/bin/sidekiqctl +0 -20
  150. data/code_of_conduct.md +0 -50
  151. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  152. data/lib/sidekiq/core_ext.rb +0 -1
  153. data/lib/sidekiq/ctl.rb +0 -221
  154. data/lib/sidekiq/delay.rb +0 -42
  155. data/lib/sidekiq/exception_handler.rb +0 -29
  156. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  157. data/lib/sidekiq/extensions/active_record.rb +0 -40
  158. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  159. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  160. data/lib/sidekiq/logging.rb +0 -122
  161. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  162. data/lib/sidekiq/util.rb +0 -66
  163. data/lib/sidekiq/worker.rb +0 -220
  164. data/web/assets/stylesheets/application-rtl.css +0 -246
  165. data/web/assets/stylesheets/application.css +0 -1144
  166. data/web/assets/stylesheets/bootstrap-rtl.min.css +0 -9
  167. data/web/assets/stylesheets/bootstrap.css +0 -5
  168. data/web/locales/zh-cn.yml +0 -68
  169. data/web/views/_status.erb +0 -4
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/scheduled'
3
- require 'sidekiq/api'
2
+
3
+ require "zlib"
4
+ require "sidekiq/component"
4
5
 
5
6
  module Sidekiq
6
7
  ##
@@ -21,18 +22,19 @@ module Sidekiq
21
22
  #
22
23
  # A job looks like:
23
24
  #
24
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
25
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
25
26
  #
26
27
  # The 'retry' option also accepts a number (in place of 'true'):
27
28
  #
28
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
29
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
29
30
  #
30
31
  # The job will be retried this number of times before giving up. (If simply
31
32
  # 'true', Sidekiq retries 25 times)
32
33
  #
33
- # We'll add a bit more data to the job to support retries:
34
+ # Relevant options for job retries:
34
35
  #
35
- # * '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
36
38
  # * 'retry_count' - number of times we've retried so far.
37
39
  # * 'error_message' - the message from the exception
38
40
  # * 'error_class' - the exception class
@@ -46,31 +48,40 @@ module Sidekiq
46
48
  # The default number of retries is 25 which works out to about 3 weeks
47
49
  # You can change the default maximum number of retries in your initializer:
48
50
  #
49
- # Sidekiq.options[:max_retries] = 7
51
+ # Sidekiq.default_configuration[:max_retries] = 7
50
52
  #
51
- # 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:
52
55
  #
53
- # class MyWorker
54
- # include Sidekiq::Worker
55
- # sidekiq_options :retry => 10
56
+ # class MyJob
57
+ # include Sidekiq::Job
58
+ # sidekiq_options retry: 10, retry_queue: 'low'
56
59
  # end
57
60
  #
58
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.
59
65
  class Handled < ::RuntimeError; end
66
+
67
+ # Skip means the job failed but Sidekiq does not need to
68
+ # create a retry, log it or send to error_handlers.
60
69
  class Skip < Handled; end
61
70
 
62
- include Sidekiq::Util
71
+ include Sidekiq::Component
63
72
 
64
73
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
65
74
 
66
- def initialize(options = {})
67
- @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]
68
79
  end
69
80
 
70
81
  # The global retry handler requires only the barest of data.
71
82
  # We want to be able to retry as much as possible so we don't
72
- # require the worker to be instantiated.
73
- def global(msg, queue)
83
+ # require the job to be instantiated.
84
+ def global(jobstr, queue)
74
85
  yield
75
86
  rescue Handled => ex
76
87
  raise ex
@@ -81,31 +92,29 @@ module Sidekiq
81
92
  # ignore, will be pushed back onto queue during hard_shutdown
82
93
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
83
94
 
84
- if msg['retry']
85
- attempt_retry(nil, msg, queue, e)
95
+ msg = Sidekiq.load_json(jobstr)
96
+ if msg["retry"]
97
+ process_retry(nil, msg, queue, e)
86
98
  else
87
- Sidekiq.death_handlers.each do |handler|
88
- begin
89
- handler.call(msg, e)
90
- rescue => handler_ex
91
- handle_exception(handler_ex, { context: "Error calling death handler", job: msg })
92
- end
99
+ @capsule.config.death_handlers.each do |handler|
100
+ handler.call(msg, e)
101
+ rescue => handler_ex
102
+ handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
93
103
  end
94
104
  end
95
105
 
96
106
  raise Handled
97
107
  end
98
108
 
99
-
100
109
  # The local retry support means that any errors that occur within
101
- # this block can be associated with the given worker instance.
110
+ # this block can be associated with the given job instance.
102
111
  # This is required to support the `sidekiq_retries_exhausted` block.
103
112
  #
104
113
  # Note that any exception from the block is wrapped in the Skip
105
114
  # exception so the global block does not reprocess the error. The
106
115
  # Skip exception is unwrapped within Sidekiq::Processor#process before
107
116
  # calling the handle_exception handlers.
108
- def local(worker, msg, queue)
117
+ def local(jobinst, jobstr, queue)
109
118
  yield
110
119
  rescue Handled => ex
111
120
  raise ex
@@ -116,93 +125,158 @@ module Sidekiq
116
125
  # ignore, will be pushed back onto queue during hard_shutdown
117
126
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
118
127
 
119
- if msg['retry'] == nil
120
- msg['retry'] = worker.class.get_sidekiq_options['retry']
128
+ msg = Sidekiq.load_json(jobstr)
129
+ if msg["retry"].nil?
130
+ msg["retry"] = jobinst.class.get_sidekiq_options["retry"]
121
131
  end
122
132
 
123
- raise e unless msg['retry']
124
- attempt_retry(worker, msg, queue, e)
133
+ raise e unless msg["retry"]
134
+ process_retry(jobinst, msg, queue, e)
125
135
  # We've handled this error associated with this job, don't
126
136
  # need to handle it at the global level
127
- raise Skip
137
+ raise Handled
128
138
  end
129
139
 
130
140
  private
131
141
 
132
- # Note that +worker+ can be nil here if an error is raised before we can
133
- # 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
134
148
  # best effort.
135
- def attempt_retry(worker, msg, queue, exception)
136
- max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
149
+ def process_retry(jobinst, msg, queue, exception)
150
+ max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
137
151
 
138
- msg['queue'] = if msg['retry_queue']
139
- msg['retry_queue']
140
- else
141
- queue
142
- end
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
- msg['error_message'] = m
151
- msg['error_class'] = exception.class.name
152
- count = if msg['retry_count']
153
- msg['retried_at'] = Time.now.to_f
154
- msg['retry_count'] += 1
160
+ msg["error_message"] = m
161
+ msg["error_class"] = exception.class.name
162
+ count = if msg["retry_count"]
163
+ msg["retried_at"] = now_ms
164
+ msg["retry_count"] += 1
155
165
  else
156
- msg['failed_at'] = Time.now.to_f
157
- msg['retry_count'] = 0
166
+ msg["failed_at"] = now_ms
167
+ msg["retry_count"] = 0
158
168
  end
159
169
 
160
- if msg['backtrace'] == true
161
- msg['error_backtrace'] = exception.backtrace
162
- elsif !msg['backtrace']
163
- # do nothing
164
- elsif msg['backtrace'].to_i != 0
165
- msg['error_backtrace'] = exception.backtrace[0...msg['backtrace'].to_i]
170
+ if msg["backtrace"]
171
+ backtrace = @backtrace_cleaner.call(exception.backtrace)
172
+ lines = if msg["backtrace"] == true
173
+ backtrace
174
+ else
175
+ backtrace[0...msg["backtrace"].to_i]
176
+ end
177
+
178
+ msg["error_backtrace"] = compress_backtrace(lines)
166
179
  end
167
180
 
168
- if count < max_retry_attempts
169
- delay = delay_for(worker, count, exception)
170
- # Logging here can break retries if the logging device raises ENOSPC #3979
171
- #logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
172
- retry_at = Time.now.to_f + delay
173
- payload = Sidekiq.dump_json(msg)
174
- Sidekiq.redis do |conn|
175
- conn.zadd('retry', retry_at.to_s, payload)
176
- end
181
+ return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
182
+
183
+ rf = msg["retry_for"]
184
+ return retries_exhausted(jobinst, msg, exception) if rf && (time_for(msg["failed_at"]) + rf) < Time.now
185
+
186
+ strategy, delay = delay_for(jobinst, count, exception, msg)
187
+ case strategy
188
+ when :discard
189
+ return # poof!
190
+ when :kill
191
+ return retries_exhausted(jobinst, msg, exception)
192
+ end
193
+
194
+ # Logging here can break retries if the logging device raises ENOSPC #3979
195
+ # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
196
+ jitter = rand(10 * (count + 1))
197
+ retry_at = Time.now.to_f + delay + jitter
198
+ payload = Sidekiq.dump_json(msg)
199
+ redis do |conn|
200
+ conn.zadd("retry", retry_at.to_s, payload)
201
+ end
202
+ end
203
+
204
+ def time_for(item)
205
+ if item.is_a?(Float)
206
+ Time.at(item)
177
207
  else
178
- # Goodbye dear message, you (re)tried your best I'm sure.
179
- retries_exhausted(worker, msg, exception)
208
+ Time.at(item / 1000, item % 1000)
180
209
  end
181
210
  end
182
211
 
183
- def retries_exhausted(worker, msg, exception)
184
- begin
185
- block = worker && worker.sidekiq_retries_exhausted_block
186
- block.call(msg, exception) if block
187
- rescue => e
188
- handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
212
+ # returns (strategy, seconds)
213
+ def delay_for(jobinst, count, exception, msg)
214
+ rv = begin
215
+ # sidekiq_retry_in can return two different things:
216
+ # 1. When to retry next, as an integer of seconds
217
+ # 2. A symbol which re-routes the job elsewhere, e.g. :discard, :kill, :default
218
+ block = jobinst&.sidekiq_retry_in_block
219
+
220
+ # the sidekiq_retry_in_block can be defined in a wrapped class (ActiveJob for instance)
221
+ unless msg["wrapped"].nil?
222
+ wrapped = Object.const_get(msg["wrapped"])
223
+ block = wrapped.respond_to?(:sidekiq_retry_in_block) ? wrapped.sidekiq_retry_in_block : nil
224
+ end
225
+ block&.call(count, exception, msg)
226
+ rescue Exception => e
227
+ handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
228
+ nil
189
229
  end
190
230
 
191
- Sidekiq.death_handlers.each do |handler|
192
- begin
193
- handler.call(msg, exception)
194
- rescue => e
195
- handle_exception(e, { context: "Error calling death handler", job: msg })
231
+ rv = rv.to_i if rv.respond_to?(:to_i)
232
+ delay = (count**4) + 15
233
+ if Integer === rv && rv > 0
234
+ delay = rv
235
+ elsif rv == :discard
236
+ return [:discard, nil] # do nothing, job goes poof
237
+ elsif rv == :kill
238
+ return [:kill, nil]
239
+ end
240
+
241
+ [:default, delay]
242
+ end
243
+
244
+ def retries_exhausted(jobinst, msg, exception)
245
+ rv = begin
246
+ block = jobinst&.sidekiq_retries_exhausted_block
247
+
248
+ # the sidekiq_retries_exhausted_block can be defined in a wrapped class (ActiveJob for instance)
249
+ unless msg["wrapped"].nil?
250
+ wrapped = Object.const_get(msg["wrapped"])
251
+ block = wrapped.respond_to?(:sidekiq_retries_exhausted_block) ? wrapped.sidekiq_retries_exhausted_block : nil
196
252
  end
253
+ block&.call(msg, exception)
254
+ rescue => e
255
+ handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
197
256
  end
198
257
 
199
- send_to_morgue(msg) unless msg['dead'] == false
258
+ return if rv == :discard # poof!
259
+ send_to_morgue(msg) unless msg["dead"] == false
260
+
261
+ @capsule.config.death_handlers.each do |handler|
262
+ handler.call(msg, exception)
263
+ rescue => e
264
+ handle_exception(e, {context: "Error calling death handler", job: msg})
265
+ end
200
266
  end
201
267
 
202
268
  def send_to_morgue(msg)
203
- logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
269
+ logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
204
270
  payload = Sidekiq.dump_json(msg)
205
- DeadSet.new.kill(payload, notify_failure: false)
271
+ now = Time.now.to_f
272
+
273
+ redis do |conn|
274
+ conn.multi do |xa|
275
+ xa.zadd("dead", now.to_s, payload)
276
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
277
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
278
+ end
279
+ end
206
280
  end
207
281
 
208
282
  def retry_attempts_from(msg_retry, default)
@@ -213,28 +287,6 @@ module Sidekiq
213
287
  end
214
288
  end
215
289
 
216
- def delay_for(worker, count, exception)
217
- if worker && worker.sidekiq_retry_in_block
218
- custom_retry_in = retry_in(worker, count, exception).to_i
219
- return custom_retry_in if custom_retry_in > 0
220
- end
221
- seconds_to_delay(count)
222
- end
223
-
224
- # delayed_job uses the same basic formula
225
- def seconds_to_delay(count)
226
- (count ** 4) + 15 + (rand(30)*(count+1))
227
- end
228
-
229
- def retry_in(worker, count, exception)
230
- begin
231
- worker.sidekiq_retry_in_block.call(count, exception)
232
- rescue Exception => e
233
- handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
234
- nil
235
- end
236
- end
237
-
238
290
  def exception_caused_by_shutdown?(e, checked_causes = [])
239
291
  return false unless e.cause
240
292
 
@@ -249,14 +301,17 @@ module Sidekiq
249
301
  # Extract message from exception.
250
302
  # Set a default if the message raises an error
251
303
  def exception_message(exception)
252
- begin
253
- # App code can stuff all sorts of crazy binary data into the error message
254
- # that won't convert to JSON.
255
- exception.message.to_s[0, 10_000]
256
- rescue
257
- "!!! ERROR MESSAGE THREW AN ERROR !!!".dup
258
- end
304
+ # App code can stuff all sorts of crazy binary data into the error message
305
+ # that won't convert to JSON.
306
+ exception.message.to_s[0, 10_000]
307
+ rescue
308
+ +"!!! ERROR MESSAGE THREW AN ERROR !!!"
259
309
  end
260
310
 
311
+ def compress_backtrace(backtrace)
312
+ serialized = Sidekiq.dump_json(backtrace)
313
+ compressed = Zlib::Deflate.deflate(serialized)
314
+ [compressed].pack("m0") # Base64.strict_encode64
315
+ end
261
316
  end
262
317
  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