sidekiq 5.2.1 → 6.4.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 (106) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +368 -1
  3. data/LICENSE +3 -3
  4. data/README.md +21 -37
  5. data/bin/sidekiq +26 -2
  6. data/bin/sidekiqload +33 -25
  7. data/bin/sidekiqmon +8 -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 +316 -246
  13. data/lib/sidekiq/cli.rb +195 -221
  14. data/lib/sidekiq/client.rb +42 -60
  15. data/lib/sidekiq/delay.rb +7 -6
  16. data/lib/sidekiq/exception_handler.rb +10 -12
  17. data/lib/sidekiq/extensions/action_mailer.rb +15 -24
  18. data/lib/sidekiq/extensions/active_record.rb +15 -12
  19. data/lib/sidekiq/extensions/class_methods.rb +16 -13
  20. data/lib/sidekiq/extensions/generic_proxy.rb +8 -6
  21. data/lib/sidekiq/fetch.rb +39 -31
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +47 -9
  24. data/lib/sidekiq/job_retry.rb +88 -68
  25. data/lib/sidekiq/job_util.rb +65 -0
  26. data/lib/sidekiq/launcher.rb +151 -61
  27. data/lib/sidekiq/logger.rb +166 -0
  28. data/lib/sidekiq/manager.rb +18 -22
  29. data/lib/sidekiq/middleware/chain.rb +20 -8
  30. data/lib/sidekiq/middleware/current_attributes.rb +57 -0
  31. data/lib/sidekiq/middleware/i18n.rb +5 -7
  32. data/lib/sidekiq/monitor.rb +133 -0
  33. data/lib/sidekiq/paginator.rb +18 -14
  34. data/lib/sidekiq/processor.rb +116 -82
  35. data/lib/sidekiq/rails.rb +42 -38
  36. data/lib/sidekiq/redis_connection.rb +49 -30
  37. data/lib/sidekiq/scheduled.rb +62 -28
  38. data/lib/sidekiq/sd_notify.rb +149 -0
  39. data/lib/sidekiq/systemd.rb +24 -0
  40. data/lib/sidekiq/testing/inline.rb +2 -1
  41. data/lib/sidekiq/testing.rb +36 -27
  42. data/lib/sidekiq/util.rb +57 -15
  43. data/lib/sidekiq/version.rb +2 -1
  44. data/lib/sidekiq/web/action.rb +15 -11
  45. data/lib/sidekiq/web/application.rb +95 -76
  46. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  47. data/lib/sidekiq/web/helpers.rb +115 -91
  48. data/lib/sidekiq/web/router.rb +23 -19
  49. data/lib/sidekiq/web.rb +61 -105
  50. data/lib/sidekiq/worker.rb +259 -99
  51. data/lib/sidekiq.rb +79 -45
  52. data/sidekiq.gemspec +23 -18
  53. data/web/assets/images/apple-touch-icon.png +0 -0
  54. data/web/assets/javascripts/application.js +83 -64
  55. data/web/assets/javascripts/dashboard.js +66 -75
  56. data/web/assets/stylesheets/application-dark.css +143 -0
  57. data/web/assets/stylesheets/application-rtl.css +0 -4
  58. data/web/assets/stylesheets/application.css +75 -231
  59. data/web/assets/stylesheets/bootstrap.css +1 -1
  60. data/web/locales/ar.yml +9 -2
  61. data/web/locales/de.yml +14 -2
  62. data/web/locales/en.yml +7 -1
  63. data/web/locales/es.yml +18 -2
  64. data/web/locales/fr.yml +10 -3
  65. data/web/locales/ja.yml +7 -1
  66. data/web/locales/lt.yml +83 -0
  67. data/web/locales/pl.yml +4 -4
  68. data/web/locales/ru.yml +4 -0
  69. data/web/locales/vi.yml +83 -0
  70. data/web/views/_footer.erb +1 -1
  71. data/web/views/_job_info.erb +3 -2
  72. data/web/views/_nav.erb +3 -17
  73. data/web/views/_poll_link.erb +2 -5
  74. data/web/views/_summary.erb +7 -7
  75. data/web/views/busy.erb +54 -20
  76. data/web/views/dashboard.erb +22 -14
  77. data/web/views/dead.erb +3 -3
  78. data/web/views/layout.erb +3 -1
  79. data/web/views/morgue.erb +9 -6
  80. data/web/views/queue.erb +20 -10
  81. data/web/views/queues.erb +11 -3
  82. data/web/views/retries.erb +14 -7
  83. data/web/views/retry.erb +3 -3
  84. data/web/views/scheduled.erb +5 -2
  85. metadata +39 -54
  86. data/.github/contributing.md +0 -32
  87. data/.github/issue_template.md +0 -11
  88. data/.gitignore +0 -13
  89. data/.travis.yml +0 -14
  90. data/3.0-Upgrade.md +0 -70
  91. data/4.0-Upgrade.md +0 -53
  92. data/5.0-Upgrade.md +0 -56
  93. data/COMM-LICENSE +0 -95
  94. data/Ent-Changes.md +0 -221
  95. data/Gemfile +0 -14
  96. data/Pro-2.0-Upgrade.md +0 -138
  97. data/Pro-3.0-Upgrade.md +0 -44
  98. data/Pro-4.0-Upgrade.md +0 -35
  99. data/Pro-Changes.md +0 -739
  100. data/Rakefile +0 -8
  101. data/bin/sidekiqctl +0 -99
  102. data/code_of_conduct.md +0 -50
  103. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  104. data/lib/sidekiq/core_ext.rb +0 -1
  105. data/lib/sidekiq/logging.rb +0 -122
  106. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
@@ -1,25 +1,63 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Sidekiq
3
4
  class JobLogger
5
+ def initialize(logger = Sidekiq.logger)
6
+ @logger = logger
7
+ end
4
8
 
5
9
  def call(item, queue)
6
- start = Time.now
7
- logger.info("start")
10
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
11
+ @logger.info("start")
12
+
8
13
  yield
9
- logger.info("done: #{elapsed(start)} sec")
14
+
15
+ with_elapsed_time_context(start) do
16
+ @logger.info("done")
17
+ end
10
18
  rescue Exception
11
- logger.info("fail: #{elapsed(start)} sec")
19
+ with_elapsed_time_context(start) do
20
+ @logger.info("fail")
21
+ end
22
+
12
23
  raise
13
24
  end
14
25
 
15
- private
26
+ def prepare(job_hash, &block)
27
+ level = job_hash["log_level"]
28
+ if level
29
+ @logger.log_at(level) do
30
+ Sidekiq::Context.with(job_hash_context(job_hash), &block)
31
+ end
32
+ else
33
+ Sidekiq::Context.with(job_hash_context(job_hash), &block)
34
+ end
35
+ end
16
36
 
17
- def elapsed(start)
18
- (Time.now - start).round(3)
37
+ def job_hash_context(job_hash)
38
+ # If we're using a wrapper class, like ActiveJob, use the "wrapped"
39
+ # attribute to expose the underlying thing.
40
+ h = {
41
+ class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
42
+ jid: job_hash["jid"]
43
+ }
44
+ h[:bid] = job_hash["bid"] if job_hash["bid"]
45
+ h[:tags] = job_hash["tags"] if job_hash["tags"]
46
+ h
19
47
  end
20
48
 
21
- def logger
22
- Sidekiq.logger
49
+ def with_elapsed_time_context(start, &block)
50
+ Sidekiq::Context.with(elapsed_time_context(start), &block)
51
+ end
52
+
53
+ def elapsed_time_context(start)
54
+ {elapsed: elapsed(start).to_s}
55
+ end
56
+
57
+ private
58
+
59
+ def elapsed(start)
60
+ (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start).round(3)
23
61
  end
24
62
  end
25
63
  end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
- require 'sidekiq/scheduled'
3
- require 'sidekiq/api'
2
+
3
+ require "sidekiq/scheduled"
4
+ require "sidekiq/api"
5
+
6
+ require "zlib"
7
+ require "base64"
4
8
 
5
9
  module Sidekiq
6
10
  ##
@@ -30,9 +34,10 @@ module Sidekiq
30
34
  # The job will be retried this number of times before giving up. (If simply
31
35
  # 'true', Sidekiq retries 25 times)
32
36
  #
33
- # We'll add a bit more data to the job to support retries:
37
+ # Relevant options for job retries:
34
38
  #
35
- # * 'queue' - the queue to use
39
+ # * 'queue' - the queue for the initial job
40
+ # * 'retry_queue' - if job retries should be pushed to a different (e.g. lower priority) queue
36
41
  # * 'retry_count' - number of times we've retried so far.
37
42
  # * 'error_message' - the message from the exception
38
43
  # * 'error_class' - the exception class
@@ -48,15 +53,18 @@ module Sidekiq
48
53
  #
49
54
  # Sidekiq.options[:max_retries] = 7
50
55
  #
51
- # or limit the number of retries for a particular worker with:
56
+ # or limit the number of retries for a particular worker and send retries to
57
+ # a low priority queue with:
52
58
  #
53
59
  # class MyWorker
54
60
  # include Sidekiq::Worker
55
- # sidekiq_options :retry => 10
61
+ # sidekiq_options retry: 10, retry_queue: 'low'
56
62
  # end
57
63
  #
58
64
  class JobRetry
59
- class Skip < ::RuntimeError; end
65
+ class Handled < ::RuntimeError; end
66
+
67
+ class Skip < Handled; end
60
68
 
61
69
  include Sidekiq::Util
62
70
 
@@ -69,9 +77,9 @@ module Sidekiq
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
79
  # require the worker to be instantiated.
72
- def global(msg, queue)
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,11 +88,19 @@ 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
+ attempt_retry(nil, msg, queue, e)
94
+ else
95
+ Sidekiq.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
106
  # this block can be associated with the given worker instance.
@@ -94,9 +110,9 @@ module Sidekiq
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(worker, 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,11 +121,12 @@ 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"] = worker.class.get_sidekiq_options["retry"]
110
127
  end
111
128
 
112
- raise e unless msg['retry']
129
+ raise e unless msg["retry"]
113
130
  attempt_retry(worker, 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
@@ -122,47 +139,44 @@ module Sidekiq
122
139
  # instantiate the worker instance. All access must be guarded and
123
140
  # best effort.
124
141
  def attempt_retry(worker, msg, queue, exception)
125
- max_retry_attempts = retry_attempts_from(msg['retry'], @max_retries)
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
149
160
  end
150
161
 
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]
162
+ if msg["backtrace"]
163
+ lines = if msg["backtrace"] == true
164
+ exception.backtrace
165
+ else
166
+ exception.backtrace[0...msg["backtrace"].to_i]
167
+ end
168
+
169
+ msg["error_backtrace"] = compress_backtrace(lines)
157
170
  end
158
171
 
159
172
  if count < max_retry_attempts
160
173
  delay = delay_for(worker, count, exception)
161
- logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
174
+ # Logging here can break retries if the logging device raises ENOSPC #3979
175
+ # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
162
176
  retry_at = Time.now.to_f + delay
163
177
  payload = Sidekiq.dump_json(msg)
164
178
  Sidekiq.redis do |conn|
165
- conn.zadd('retry', retry_at.to_s, payload)
179
+ conn.zadd("retry", retry_at.to_s, payload)
166
180
  end
167
181
  else
168
182
  # Goodbye dear message, you (re)tried your best I'm sure.
@@ -171,27 +185,24 @@ module Sidekiq
171
185
  end
172
186
 
173
187
  def retries_exhausted(worker, msg, exception)
174
- logger.debug { "Retries exhausted for job" }
175
188
  begin
176
- block = worker && worker.sidekiq_retries_exhausted_block
177
- block.call(msg, exception) if block
189
+ block = worker&.sidekiq_retries_exhausted_block
190
+ block&.call(msg, exception)
178
191
  rescue => e
179
- handle_exception(e, { context: "Error calling retries_exhausted", job: msg })
192
+ handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
180
193
  end
181
194
 
195
+ send_to_morgue(msg) unless msg["dead"] == false
196
+
182
197
  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 })
187
- end
198
+ handler.call(msg, exception)
199
+ rescue => e
200
+ handle_exception(e, {context: "Error calling death handler", job: msg})
188
201
  end
189
-
190
- send_to_morgue(msg) unless msg['dead'] == false
191
202
  end
192
203
 
193
204
  def send_to_morgue(msg)
194
- Sidekiq.logger.info { "Adding dead #{msg['class']} job #{msg['jid']}" }
205
+ logger.info { "Adding dead #{msg["class"]} job #{msg["jid"]}" }
195
206
  payload = Sidekiq.dump_json(msg)
196
207
  DeadSet.new.kill(payload, notify_failure: false)
197
208
  end
@@ -205,25 +216,19 @@ module Sidekiq
205
216
  end
206
217
 
207
218
  def delay_for(worker, count, exception)
208
- if worker && worker.sidekiq_retry_in_block
219
+ jitter = rand(10) * (count + 1)
220
+ if worker&.sidekiq_retry_in_block
209
221
  custom_retry_in = retry_in(worker, count, exception).to_i
210
- return custom_retry_in if custom_retry_in > 0
222
+ return custom_retry_in + jitter if custom_retry_in > 0
211
223
  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))
224
+ (count**4) + 15 + jitter
218
225
  end
219
226
 
220
227
  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
228
+ worker.sidekiq_retry_in_block.call(count, exception)
229
+ rescue Exception => e
230
+ handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"})
231
+ nil
227
232
  end
228
233
 
229
234
  def exception_caused_by_shutdown?(e, checked_causes = [])
@@ -237,5 +242,20 @@ module Sidekiq
237
242
  exception_caused_by_shutdown?(e.cause, checked_causes)
238
243
  end
239
244
 
245
+ # Extract message from exception.
246
+ # Set a default if the message raises an error
247
+ def exception_message(exception)
248
+ # App code can stuff all sorts of crazy binary data into the error message
249
+ # that won't convert to JSON.
250
+ exception.message.to_s[0, 10_000]
251
+ rescue
252
+ +"!!! ERROR MESSAGE THREW AN ERROR !!!"
253
+ end
254
+
255
+ def compress_backtrace(backtrace)
256
+ serialized = Sidekiq.dump_json(backtrace)
257
+ compressed = Zlib::Deflate.deflate(serialized)
258
+ Base64.encode64(compressed)
259
+ end
240
260
  end
241
261
  end
@@ -0,0 +1,65 @@
1
+ require "securerandom"
2
+ require "time"
3
+
4
+ module Sidekiq
5
+ module JobUtil
6
+ # These functions encapsulate various job utilities.
7
+ # They must be simple and free from side effects.
8
+
9
+ def validate(item)
10
+ raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
11
+ raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
12
+ 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)
13
+ raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
14
+ raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
15
+
16
+ if Sidekiq.options[:on_complex_arguments] == :raise
17
+ msg = <<~EOM
18
+ Job arguments to #{item["class"]} must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices.
19
+ To disable this error, remove `Sidekiq.strict_args!` from your initializer.
20
+ EOM
21
+ raise(ArgumentError, msg) unless json_safe?(item)
22
+ elsif Sidekiq.options[:on_complex_arguments] == :warn
23
+ Sidekiq.logger.warn <<~EOM unless json_safe?(item)
24
+ Job arguments to #{item["class"]} do not serialize to JSON safely. This will raise an error in
25
+ Sidekiq 7.0. See https://github.com/mperham/sidekiq/wiki/Best-Practices or raise an error today
26
+ by calling `Sidekiq.strict_args!` during Sidekiq initialization.
27
+ EOM
28
+ end
29
+ end
30
+
31
+ def normalize_item(item)
32
+ validate(item)
33
+
34
+ # merge in the default sidekiq_options for the item's class and/or wrapped element
35
+ # this allows ActiveJobs to control sidekiq_options too.
36
+ defaults = normalized_hash(item["class"])
37
+ defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?(:get_sidekiq_options)
38
+ item = defaults.merge(item)
39
+
40
+ raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
41
+
42
+ item["class"] = item["class"].to_s
43
+ item["queue"] = item["queue"].to_s
44
+ item["jid"] ||= SecureRandom.hex(12)
45
+ item["created_at"] ||= Time.now.to_f
46
+
47
+ item
48
+ end
49
+
50
+ def normalized_hash(item_class)
51
+ if item_class.is_a?(Class)
52
+ raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?(:get_sidekiq_options)
53
+ item_class.get_sidekiq_options
54
+ else
55
+ Sidekiq.default_worker_options
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def json_safe?(item)
62
+ JSON.parse(JSON.dump(item["args"])) == item["args"]
63
+ end
64
+ end
65
+ end