sidekiq 5.2.2 → 7.2.0

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