sidekiq 5.2.6 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq might be problematic. Click here for more details.

Files changed (148) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +537 -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 +556 -351
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +203 -226
  15. data/lib/sidekiq/client.rb +121 -101
  16. data/lib/sidekiq/component.rb +68 -0
  17. data/lib/sidekiq/config.rb +274 -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 +33 -7
  23. data/lib/sidekiq/job_retry.rb +131 -108
  24. data/lib/sidekiq/job_util.rb +105 -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 +153 -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 +56 -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 +108 -107
  38. data/lib/sidekiq/rails.rb +49 -38
  39. data/lib/sidekiq/redis_client_adapter.rb +96 -0
  40. data/lib/sidekiq/redis_connection.rb +38 -107
  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 +66 -84
  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 +123 -79
  51. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  52. data/lib/sidekiq/web/helpers.rb +137 -106
  53. data/lib/sidekiq/web/router.rb +23 -19
  54. data/lib/sidekiq/web.rb +56 -107
  55. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  56. data/lib/sidekiq.rb +92 -182
  57. data/sidekiq.gemspec +25 -16
  58. data/web/assets/images/apple-touch-icon.png +0 -0
  59. data/web/assets/javascripts/application.js +130 -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 +166 -0
  64. data/web/assets/javascripts/dashboard.js +36 -292
  65. data/web/assets/javascripts/metrics.js +264 -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 +102 -522
  69. data/web/locales/ar.yml +71 -65
  70. data/web/locales/cs.yml +62 -62
  71. data/web/locales/da.yml +60 -53
  72. data/web/locales/de.yml +65 -53
  73. data/web/locales/el.yml +43 -24
  74. data/web/locales/en.yml +84 -66
  75. data/web/locales/es.yml +70 -54
  76. data/web/locales/fa.yml +65 -65
  77. data/web/locales/fr.yml +83 -62
  78. data/web/locales/gd.yml +99 -0
  79. data/web/locales/he.yml +65 -64
  80. data/web/locales/hi.yml +59 -59
  81. data/web/locales/it.yml +53 -53
  82. data/web/locales/ja.yml +75 -64
  83. data/web/locales/ko.yml +52 -52
  84. data/web/locales/lt.yml +83 -0
  85. data/web/locales/nb.yml +61 -61
  86. data/web/locales/nl.yml +52 -52
  87. data/web/locales/pl.yml +45 -45
  88. data/web/locales/pt-br.yml +63 -55
  89. data/web/locales/pt.yml +51 -51
  90. data/web/locales/ru.yml +68 -63
  91. data/web/locales/sv.yml +53 -53
  92. data/web/locales/ta.yml +60 -60
  93. data/web/locales/uk.yml +62 -61
  94. data/web/locales/ur.yml +64 -64
  95. data/web/locales/vi.yml +83 -0
  96. data/web/locales/zh-cn.yml +43 -16
  97. data/web/locales/zh-tw.yml +42 -8
  98. data/web/views/_footer.erb +6 -3
  99. data/web/views/_job_info.erb +21 -4
  100. data/web/views/_metrics_period_select.erb +12 -0
  101. data/web/views/_nav.erb +1 -1
  102. data/web/views/_paging.erb +2 -0
  103. data/web/views/_poll_link.erb +3 -6
  104. data/web/views/_summary.erb +7 -7
  105. data/web/views/busy.erb +75 -25
  106. data/web/views/dashboard.erb +58 -18
  107. data/web/views/dead.erb +3 -3
  108. data/web/views/layout.erb +3 -1
  109. data/web/views/metrics.erb +82 -0
  110. data/web/views/metrics_for_job.erb +68 -0
  111. data/web/views/morgue.erb +14 -15
  112. data/web/views/queue.erb +33 -24
  113. data/web/views/queues.erb +13 -3
  114. data/web/views/retries.erb +16 -17
  115. data/web/views/retry.erb +3 -3
  116. data/web/views/scheduled.erb +17 -15
  117. metadata +69 -69
  118. data/.github/contributing.md +0 -32
  119. data/.github/issue_template.md +0 -11
  120. data/.gitignore +0 -15
  121. data/.travis.yml +0 -11
  122. data/3.0-Upgrade.md +0 -70
  123. data/4.0-Upgrade.md +0 -53
  124. data/5.0-Upgrade.md +0 -56
  125. data/COMM-LICENSE +0 -97
  126. data/Ent-Changes.md +0 -238
  127. data/Gemfile +0 -23
  128. data/LICENSE +0 -9
  129. data/Pro-2.0-Upgrade.md +0 -138
  130. data/Pro-3.0-Upgrade.md +0 -44
  131. data/Pro-4.0-Upgrade.md +0 -35
  132. data/Pro-Changes.md +0 -759
  133. data/Rakefile +0 -9
  134. data/bin/sidekiqctl +0 -20
  135. data/code_of_conduct.md +0 -50
  136. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  137. data/lib/sidekiq/core_ext.rb +0 -1
  138. data/lib/sidekiq/ctl.rb +0 -221
  139. data/lib/sidekiq/delay.rb +0 -42
  140. data/lib/sidekiq/exception_handler.rb +0 -29
  141. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  142. data/lib/sidekiq/extensions/active_record.rb +0 -40
  143. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  144. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  145. data/lib/sidekiq/logging.rb +0 -122
  146. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  147. data/lib/sidekiq/util.rb +0 -66
  148. data/lib/sidekiq/worker.rb +0 -220
@@ -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,31 +49,35 @@ 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
63
  class Handled < ::RuntimeError; end
64
+
60
65
  class Skip < Handled; end
61
66
 
62
- include Sidekiq::Util
67
+ include Sidekiq::Component
63
68
 
64
69
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
65
70
 
66
- def initialize(options = {})
67
- @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]
68
75
  end
69
76
 
70
77
  # The global retry handler requires only the barest of data.
71
78
  # 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)
79
+ # require the job to be instantiated.
80
+ def global(jobstr, queue)
74
81
  yield
75
82
  rescue Handled => ex
76
83
  raise ex
@@ -81,31 +88,29 @@ module Sidekiq
81
88
  # ignore, will be pushed back onto queue during hard_shutdown
82
89
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
83
90
 
84
- if msg['retry']
85
- attempt_retry(nil, msg, queue, e)
91
+ msg = Sidekiq.load_json(jobstr)
92
+ if msg["retry"]
93
+ process_retry(nil, msg, queue, e)
86
94
  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
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})
93
99
  end
94
100
  end
95
101
 
96
102
  raise Handled
97
103
  end
98
104
 
99
-
100
105
  # The local retry support means that any errors that occur within
101
- # this block can be associated with the given worker instance.
106
+ # this block can be associated with the given job instance.
102
107
  # This is required to support the `sidekiq_retries_exhausted` block.
103
108
  #
104
109
  # Note that any exception from the block is wrapped in the Skip
105
110
  # exception so the global block does not reprocess the error. The
106
111
  # Skip exception is unwrapped within Sidekiq::Processor#process before
107
112
  # calling the handle_exception handlers.
108
- def local(worker, msg, queue)
113
+ def local(jobinst, jobstr, queue)
109
114
  yield
110
115
  rescue Handled => ex
111
116
  raise ex
@@ -116,12 +121,13 @@ module Sidekiq
116
121
  # ignore, will be pushed back onto queue during hard_shutdown
117
122
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
118
123
 
119
- if msg['retry'] == nil
120
- 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"]
121
127
  end
122
128
 
123
- raise e unless msg['retry']
124
- attempt_retry(worker, msg, queue, e)
129
+ raise e unless msg["retry"]
130
+ process_retry(jobinst, msg, queue, e)
125
131
  # We've handled this error associated with this job, don't
126
132
  # need to handle it at the global level
127
133
  raise Skip
@@ -129,17 +135,13 @@ module Sidekiq
129
135
 
130
136
  private
131
137
 
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
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
134
140
  # best effort.
135
- def attempt_retry(worker, msg, queue, exception)
136
- 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)
137
143
 
138
- msg['queue'] = if msg['retry_queue']
139
- msg['retry_queue']
140
- else
141
- queue
142
- end
144
+ msg["queue"] = (msg["retry_queue"] || queue)
143
145
 
144
146
  m = exception_message(exception)
145
147
  if m.respond_to?(:scrub!)
@@ -147,62 +149,102 @@ module Sidekiq
147
149
  m.scrub!
148
150
  end
149
151
 
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
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
155
157
  else
156
- msg['failed_at'] = Time.now.to_f
157
- msg['retry_count'] = 0
158
+ msg["failed_at"] = Time.now.to_f
159
+ msg["retry_count"] = 0
158
160
  end
159
161
 
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]
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)
166
171
  end
167
172
 
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
177
- else
178
- # Goodbye dear message, you (re)tried your best I'm sure.
179
- retries_exhausted(worker, msg, exception)
173
+ # Goodbye dear message, you (re)tried your best I'm sure.
174
+ return retries_exhausted(jobinst, msg, exception) if count >= max_retry_attempts
175
+
176
+ strategy, delay = delay_for(jobinst, count, exception, msg)
177
+ case strategy
178
+ when :discard
179
+ return # poof!
180
+ when :kill
181
+ return retries_exhausted(jobinst, msg, exception)
180
182
  end
183
+
184
+ # Logging here can break retries if the logging device raises ENOSPC #3979
185
+ # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
186
+ jitter = rand(10) * (count + 1)
187
+ retry_at = Time.now.to_f + delay + jitter
188
+ payload = Sidekiq.dump_json(msg)
189
+ redis do |conn|
190
+ conn.zadd("retry", retry_at.to_s, payload)
191
+ end
192
+ end
193
+
194
+ # returns (strategy, seconds)
195
+ def delay_for(jobinst, count, exception, msg)
196
+ rv = begin
197
+ # sidekiq_retry_in can return two different things:
198
+ # 1. When to retry next, as an integer of seconds
199
+ # 2. A symbol which re-routes the job elsewhere, e.g. :discard, :kill, :default
200
+ jobinst&.sidekiq_retry_in_block&.call(count, exception, msg)
201
+ rescue Exception => e
202
+ handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{jobinst.class.name}, falling back to default"})
203
+ nil
204
+ end
205
+
206
+ rv = rv.to_i if rv.respond_to?(:to_i)
207
+ delay = (count**4) + 15
208
+ if Integer === rv && rv > 0
209
+ delay = rv
210
+ elsif rv == :discard
211
+ return [:discard, nil] # do nothing, job goes poof
212
+ elsif rv == :kill
213
+ return [:kill, nil]
214
+ end
215
+
216
+ [:default, delay]
181
217
  end
182
218
 
183
- def retries_exhausted(worker, msg, exception)
219
+ def retries_exhausted(jobinst, msg, exception)
184
220
  begin
185
- block = worker && worker.sidekiq_retries_exhausted_block
186
- block.call(msg, exception) if block
221
+ block = jobinst&.sidekiq_retries_exhausted_block
222
+ block&.call(msg, exception)
187
223
  rescue => e
188
- handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
224
+ handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
189
225
  end
190
226
 
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 })
196
- end
197
- end
227
+ send_to_morgue(msg) unless msg["dead"] == false
198
228
 
199
- send_to_morgue(msg) unless msg['dead'] == false
229
+ @capsule.config.death_handlers.each do |handler|
230
+ handler.call(msg, exception)
231
+ rescue => e
232
+ handle_exception(e, {context: "Error calling death handler", job: msg})
233
+ end
200
234
  end
201
235
 
202
236
  def send_to_morgue(msg)
203
- logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
237
+ logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
204
238
  payload = Sidekiq.dump_json(msg)
205
- DeadSet.new.kill(payload, notify_failure: false)
239
+ now = Time.now.to_f
240
+
241
+ redis do |conn|
242
+ conn.multi do |xa|
243
+ xa.zadd("dead", now.to_s, payload)
244
+ xa.zremrangebyscore("dead", "-inf", now - @capsule.config[:dead_timeout_in_seconds])
245
+ xa.zremrangebyrank("dead", 0, - @capsule.config[:dead_max_jobs])
246
+ end
247
+ end
206
248
  end
207
249
 
208
250
  def retry_attempts_from(msg_retry, default)
@@ -213,28 +255,6 @@ module Sidekiq
213
255
  end
214
256
  end
215
257
 
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
258
  def exception_caused_by_shutdown?(e, checked_causes = [])
239
259
  return false unless e.cause
240
260
 
@@ -249,14 +269,17 @@ module Sidekiq
249
269
  # Extract message from exception.
250
270
  # Set a default if the message raises an error
251
271
  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
272
+ # App code can stuff all sorts of crazy binary data into the error message
273
+ # that won't convert to JSON.
274
+ exception.message.to_s[0, 10_000]
275
+ rescue
276
+ +"!!! ERROR MESSAGE THREW AN ERROR !!!"
259
277
  end
260
278
 
279
+ def compress_backtrace(backtrace)
280
+ serialized = Sidekiq.dump_json(backtrace)
281
+ compressed = Zlib::Deflate.deflate(serialized)
282
+ Base64.encode64(compressed)
283
+ end
261
284
  end
262
285
  end
@@ -0,0 +1,105 @@
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
+ end
17
+
18
+ def verify_json(item)
19
+ job_class = item["wrapped"] || item["class"]
20
+ args = item["args"]
21
+ mode = Sidekiq::Config::DEFAULTS[:on_complex_arguments]
22
+
23
+ if mode == :raise || mode == :warn
24
+ if (unsafe_item = json_unsafe?(args))
25
+ msg = <<~EOM
26
+ Job arguments to #{job_class} must be native JSON types, but #{unsafe_item.inspect} is a #{unsafe_item.class}.
27
+ See https://github.com/sidekiq/sidekiq/wiki/Best-Practices
28
+ To disable this error, add `Sidekiq.strict_args!(false)` to your initializer.
29
+ EOM
30
+
31
+ if mode == :raise
32
+ raise(ArgumentError, msg)
33
+ else
34
+ warn(msg)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def normalize_item(item)
41
+ validate(item)
42
+
43
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
44
+ # this allows ActiveJobs to control sidekiq_options too.
45
+ defaults = normalized_hash(item["class"])
46
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?(:get_sidekiq_options)
47
+ item = defaults.merge(item)
48
+
49
+ raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
50
+
51
+ # remove job attributes which aren't necessary to persist into Redis
52
+ TRANSIENT_ATTRIBUTES.each { |key| item.delete(key) }
53
+
54
+ item["jid"] ||= SecureRandom.hex(12)
55
+ item["class"] = item["class"].to_s
56
+ item["queue"] = item["queue"].to_s
57
+ item["created_at"] ||= Time.now.to_f
58
+ item
59
+ end
60
+
61
+ def normalized_hash(item_class)
62
+ if item_class.is_a?(Class)
63
+ raise(ArgumentError, "Message must include a Sidekiq::Job class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
64
+ item_class.get_sidekiq_options
65
+ else
66
+ Sidekiq.default_job_options
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ RECURSIVE_JSON_UNSAFE = {
73
+ Integer => ->(val) {},
74
+ Float => ->(val) {},
75
+ TrueClass => ->(val) {},
76
+ FalseClass => ->(val) {},
77
+ NilClass => ->(val) {},
78
+ String => ->(val) {},
79
+ Array => ->(val) {
80
+ val.each do |e|
81
+ unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
82
+ return unsafe_item unless unsafe_item.nil?
83
+ end
84
+ nil
85
+ },
86
+ Hash => ->(val) {
87
+ val.each do |k, v|
88
+ return k unless String === k
89
+
90
+ unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
91
+ return unsafe_item unless unsafe_item.nil?
92
+ end
93
+ nil
94
+ }
95
+ }
96
+
97
+ RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
98
+ RECURSIVE_JSON_UNSAFE.compare_by_identity
99
+ private_constant :RECURSIVE_JSON_UNSAFE
100
+
101
+ def json_unsafe?(item)
102
+ RECURSIVE_JSON_UNSAFE[item.class].call(item)
103
+ end
104
+ end
105
+ end