sidekiq 5.2.4 → 7.2.4

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