sidekiq 5.1.1 → 7.1.2

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 (149) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +627 -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 +566 -329
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +241 -256
  15. data/lib/sidekiq/client.rb +125 -102
  16. data/lib/sidekiq/component.rb +68 -0
  17. data/lib/sidekiq/config.rb +278 -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 +36 -9
  23. data/lib/sidekiq/job_retry.rb +147 -98
  24. data/lib/sidekiq/job_util.rb +105 -0
  25. data/lib/sidekiq/launcher.rb +207 -103
  26. data/lib/sidekiq/logger.rb +131 -0
  27. data/lib/sidekiq/manager.rb +43 -47
  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 +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 +159 -107
  38. data/lib/sidekiq/rails.rb +54 -43
  39. data/lib/sidekiq/redis_client_adapter.rb +96 -0
  40. data/lib/sidekiq/redis_connection.rb +39 -81
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +139 -48
  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 +70 -88
  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 +143 -77
  51. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  52. data/lib/sidekiq/web/helpers.rb +144 -106
  53. data/lib/sidekiq/web/router.rb +23 -19
  54. data/lib/sidekiq/web.rb +60 -111
  55. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  56. data/lib/sidekiq.rb +94 -183
  57. data/sidekiq.gemspec +25 -23
  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 -282
  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 +134 -521
  69. data/web/assets/stylesheets/bootstrap.css +2 -2
  70. data/web/locales/ar.yml +71 -64
  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 +84 -65
  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 +63 -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 +8 -2
  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 +75 -25
  107. data/web/views/dashboard.erb +58 -18
  108. data/web/views/dead.erb +3 -3
  109. data/web/views/layout.erb +4 -2
  110. data/web/views/metrics.erb +82 -0
  111. data/web/views/metrics_for_job.erb +68 -0
  112. data/web/views/morgue.erb +14 -15
  113. data/web/views/queue.erb +33 -23
  114. data/web/views/queues.erb +14 -4
  115. data/web/views/retries.erb +19 -16
  116. data/web/views/retry.erb +3 -3
  117. data/web/views/scheduled.erb +17 -15
  118. metadata +71 -140
  119. data/.github/contributing.md +0 -32
  120. data/.github/issue_template.md +0 -11
  121. data/.gitignore +0 -13
  122. data/.travis.yml +0 -14
  123. data/3.0-Upgrade.md +0 -70
  124. data/4.0-Upgrade.md +0 -53
  125. data/5.0-Upgrade.md +0 -56
  126. data/COMM-LICENSE +0 -95
  127. data/Ent-Changes.md +0 -210
  128. data/Gemfile +0 -8
  129. data/LICENSE +0 -9
  130. data/Pro-2.0-Upgrade.md +0 -138
  131. data/Pro-3.0-Upgrade.md +0 -44
  132. data/Pro-4.0-Upgrade.md +0 -35
  133. data/Pro-Changes.md +0 -716
  134. data/Rakefile +0 -8
  135. data/bin/sidekiqctl +0 -99
  136. data/code_of_conduct.md +0 -50
  137. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  138. data/lib/sidekiq/core_ext.rb +0 -1
  139. data/lib/sidekiq/delay.rb +0 -41
  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 -22
  147. data/lib/sidekiq/middleware/server/active_record_cache.rb +0 -11
  148. data/lib/sidekiq/util.rb +0 -66
  149. data/lib/sidekiq/worker.rb +0 -204
@@ -1,5 +1,8 @@
1
- require 'sidekiq/scheduled'
2
- require 'sidekiq/api'
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "base64"
5
+ require "sidekiq/component"
3
6
 
4
7
  module Sidekiq
5
8
  ##
@@ -20,18 +23,19 @@ module Sidekiq
20
23
  #
21
24
  # A job looks like:
22
25
  #
23
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
26
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
24
27
  #
25
28
  # The 'retry' option also accepts a number (in place of 'true'):
26
29
  #
27
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
30
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
28
31
  #
29
32
  # The job will be retried this number of times before giving up. (If simply
30
33
  # 'true', Sidekiq retries 25 times)
31
34
  #
32
- # We'll add a bit more data to the job to support retries:
35
+ # Relevant options for job retries:
33
36
  #
34
- # * '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
35
39
  # * 'retry_count' - number of times we've retried so far.
36
40
  # * 'error_message' - the message from the exception
37
41
  # * 'error_class' - the exception class
@@ -45,32 +49,37 @@ module Sidekiq
45
49
  # The default number of retries is 25 which works out to about 3 weeks
46
50
  # You can change the default maximum number of retries in your initializer:
47
51
  #
48
- # Sidekiq.options[:max_retries] = 7
52
+ # Sidekiq.default_configuration[:max_retries] = 7
49
53
  #
50
- # 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:
51
56
  #
52
- # class MyWorker
53
- # include Sidekiq::Worker
54
- # sidekiq_options :retry => 10
57
+ # class MyJob
58
+ # include Sidekiq::Job
59
+ # sidekiq_options retry: 10, retry_queue: 'low'
55
60
  # end
56
61
  #
57
62
  class JobRetry
58
- class Skip < ::RuntimeError; end
63
+ class Handled < ::RuntimeError; end
64
+
65
+ class Skip < Handled; end
59
66
 
60
- include Sidekiq::Util
67
+ include Sidekiq::Component
61
68
 
62
69
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
63
70
 
64
- def initialize(options = {})
65
- @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]
66
75
  end
67
76
 
68
77
  # The global retry handler requires only the barest of data.
69
78
  # We want to be able to retry as much as possible so we don't
70
- # require the worker to be instantiated.
71
- def global(msg, queue)
79
+ # require the job to be instantiated.
80
+ def global(jobstr, queue)
72
81
  yield
73
- rescue Skip => ex
82
+ rescue Handled => ex
74
83
  raise ex
75
84
  rescue Sidekiq::Shutdown => ey
76
85
  # ignore, will be pushed back onto queue during hard_shutdown
@@ -79,23 +88,31 @@ module Sidekiq
79
88
  # ignore, will be pushed back onto queue during hard_shutdown
80
89
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
81
90
 
82
- raise e unless msg['retry']
83
- attempt_retry(nil, msg, queue, e)
84
- raise e
85
- 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
86
101
 
102
+ raise Handled
103
+ end
87
104
 
88
105
  # The local retry support means that any errors that occur within
89
- # this block can be associated with the given worker instance.
106
+ # this block can be associated with the given job instance.
90
107
  # This is required to support the `sidekiq_retries_exhausted` block.
91
108
  #
92
109
  # Note that any exception from the block is wrapped in the Skip
93
110
  # exception so the global block does not reprocess the error. The
94
111
  # Skip exception is unwrapped within Sidekiq::Processor#process before
95
112
  # calling the handle_exception handlers.
96
- def local(worker, msg, queue)
113
+ def local(jobinst, jobstr, queue)
97
114
  yield
98
- rescue Skip => ex
115
+ rescue Handled => ex
99
116
  raise ex
100
117
  rescue Sidekiq::Shutdown => ey
101
118
  # ignore, will be pushed back onto queue during hard_shutdown
@@ -104,12 +121,13 @@ module Sidekiq
104
121
  # ignore, will be pushed back onto queue during hard_shutdown
105
122
  raise Sidekiq::Shutdown if exception_caused_by_shutdown?(e)
106
123
 
107
- if msg['retry'] == nil
108
- 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"]
109
127
  end
110
128
 
111
- raise e unless msg['retry']
112
- attempt_retry(worker, msg, queue, e)
129
+ raise e unless msg["retry"]
130
+ process_retry(jobinst, msg, queue, e)
113
131
  # We've handled this error associated with this job, don't
114
132
  # need to handle it at the global level
115
133
  raise Skip
@@ -117,82 +135,116 @@ module Sidekiq
117
135
 
118
136
  private
119
137
 
120
- # Note that +worker+ can be nil here if an error is raised before we can
121
- # 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
122
140
  # best effort.
123
- def attempt_retry(worker, msg, queue, exception)
124
- 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)
125
143
 
126
- msg['queue'] = if msg['retry_queue']
127
- msg['retry_queue']
128
- else
129
- queue
130
- end
144
+ msg["queue"] = (msg["retry_queue"] || queue)
131
145
 
132
- # App code can stuff all sorts of crazy binary data into the error message
133
- # that won't convert to JSON.
134
- m = exception.message.to_s[0, 10_000]
146
+ m = exception_message(exception)
135
147
  if m.respond_to?(:scrub!)
136
148
  m.force_encoding("utf-8")
137
149
  m.scrub!
138
150
  end
139
151
 
140
- msg['error_message'] = m
141
- msg['error_class'] = exception.class.name
142
- count = if msg['retry_count']
143
- msg['retried_at'] = Time.now.to_f
144
- 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
145
157
  else
146
- msg['failed_at'] = Time.now.to_f
147
- msg['retry_count'] = 0
158
+ msg["failed_at"] = Time.now.to_f
159
+ msg["retry_count"] = 0
148
160
  end
149
161
 
150
- if msg['backtrace'] == true
151
- msg['error_backtrace'] = exception.backtrace
152
- elsif !msg['backtrace']
153
- # do nothing
154
- elsif msg['backtrace'].to_i != 0
155
- 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)
156
171
  end
157
172
 
158
- if count < max_retry_attempts
159
- delay = delay_for(worker, count, exception)
160
- logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
161
- retry_at = Time.now.to_f + delay
162
- payload = Sidekiq.dump_json(msg)
163
- Sidekiq.redis do |conn|
164
- conn.zadd('retry', retry_at.to_s, payload)
165
- end
166
- else
167
- # Goodbye dear message, you (re)tried your best I'm sure.
168
- 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)
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)
169
191
  end
170
192
  end
171
193
 
172
- def retries_exhausted(worker, msg, exception)
173
- logger.debug { "Retries exhausted for job" }
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]
217
+ end
218
+
219
+ def retries_exhausted(jobinst, msg, exception)
174
220
  begin
175
- block = worker && worker.sidekiq_retries_exhausted_block
176
- block.call(msg, exception) if block
221
+ block = jobinst&.sidekiq_retries_exhausted_block
222
+ block&.call(msg, exception)
177
223
  rescue => e
178
- handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
224
+ handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
179
225
  end
180
226
 
181
- Sidekiq.death_handlers.each do |handler|
182
- begin
183
- handler.call(msg, exception)
184
- rescue => e
185
- handle_exception(e, { context: "Error calling death handler", job: msg })
186
- end
187
- end
227
+ send_to_morgue(msg) unless msg["dead"] == false
188
228
 
189
- 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
190
234
  end
191
235
 
192
236
  def send_to_morgue(msg)
193
- Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
237
+ logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
194
238
  payload = Sidekiq.dump_json(msg)
195
- 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
196
248
  end
197
249
 
198
250
  def retry_attempts_from(msg_retry, default)
@@ -203,24 +255,6 @@ module Sidekiq
203
255
  end
204
256
  end
205
257
 
206
- def delay_for(worker, count, exception)
207
- worker && worker.sidekiq_retry_in_block && retry_in(worker, count, exception) || seconds_to_delay(count)
208
- end
209
-
210
- # delayed_job uses the same basic formula
211
- def seconds_to_delay(count)
212
- (count ** 4) + 15 + (rand(30)*(count+1))
213
- end
214
-
215
- def retry_in(worker, count, exception)
216
- begin
217
- worker.sidekiq_retry_in_block.call(count, exception).to_i
218
- rescue Exception => e
219
- handle_exception(e, { context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default" })
220
- nil
221
- end
222
- end
223
-
224
258
  def exception_caused_by_shutdown?(e, checked_causes = [])
225
259
  return false unless e.cause
226
260
 
@@ -232,5 +266,20 @@ module Sidekiq
232
266
  exception_caused_by_shutdown?(e.cause, checked_causes)
233
267
  end
234
268
 
269
+ # Extract message from exception.
270
+ # Set a default if the message raises an error
271
+ def exception_message(exception)
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 !!!"
277
+ end
278
+
279
+ def compress_backtrace(backtrace)
280
+ serialized = Sidekiq.dump_json(backtrace)
281
+ compressed = Zlib::Deflate.deflate(serialized)
282
+ Base64.encode64(compressed)
283
+ end
235
284
  end
236
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