sidekiq 6.2.2 → 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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +279 -11
  3. data/LICENSE.txt +9 -0
  4. data/README.md +45 -32
  5. data/bin/sidekiq +4 -9
  6. data/bin/sidekiqload +207 -117
  7. data/bin/sidekiqmon +4 -1
  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 +331 -187
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +94 -81
  15. data/lib/sidekiq/client.rb +100 -96
  16. data/lib/sidekiq/{util.rb → component.rb} +14 -41
  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 +26 -26
  21. data/lib/sidekiq/job.rb +371 -5
  22. data/lib/sidekiq/job_logger.rb +16 -28
  23. data/lib/sidekiq/job_retry.rb +85 -59
  24. data/lib/sidekiq/job_util.rb +105 -0
  25. data/lib/sidekiq/launcher.rb +106 -94
  26. data/lib/sidekiq/logger.rb +9 -44
  27. data/lib/sidekiq/manager.rb +40 -41
  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 +96 -51
  32. data/lib/sidekiq/middleware/current_attributes.rb +56 -0
  33. data/lib/sidekiq/middleware/i18n.rb +6 -4
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +17 -4
  36. data/lib/sidekiq/paginator.rb +17 -9
  37. data/lib/sidekiq/processor.rb +60 -60
  38. data/lib/sidekiq/rails.rb +25 -6
  39. data/lib/sidekiq/redis_client_adapter.rb +96 -0
  40. data/lib/sidekiq/redis_connection.rb +17 -88
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +101 -44
  43. data/lib/sidekiq/testing/inline.rb +4 -4
  44. data/lib/sidekiq/testing.rb +41 -68
  45. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  46. data/lib/sidekiq/version.rb +2 -1
  47. data/lib/sidekiq/web/action.rb +3 -3
  48. data/lib/sidekiq/web/application.rb +47 -13
  49. data/lib/sidekiq/web/csrf_protection.rb +3 -3
  50. data/lib/sidekiq/web/helpers.rb +36 -33
  51. data/lib/sidekiq/web.rb +10 -17
  52. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  53. data/lib/sidekiq.rb +86 -201
  54. data/sidekiq.gemspec +12 -10
  55. data/web/assets/javascripts/application.js +131 -60
  56. data/web/assets/javascripts/base-charts.js +106 -0
  57. data/web/assets/javascripts/chart.min.js +13 -0
  58. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  59. data/web/assets/javascripts/dashboard-charts.js +166 -0
  60. data/web/assets/javascripts/dashboard.js +36 -273
  61. data/web/assets/javascripts/metrics.js +264 -0
  62. data/web/assets/stylesheets/application-dark.css +23 -23
  63. data/web/assets/stylesheets/application-rtl.css +2 -95
  64. data/web/assets/stylesheets/application.css +73 -402
  65. data/web/locales/ar.yml +70 -70
  66. data/web/locales/cs.yml +62 -62
  67. data/web/locales/da.yml +60 -53
  68. data/web/locales/de.yml +65 -65
  69. data/web/locales/el.yml +43 -24
  70. data/web/locales/en.yml +82 -69
  71. data/web/locales/es.yml +68 -68
  72. data/web/locales/fa.yml +65 -65
  73. data/web/locales/fr.yml +81 -67
  74. data/web/locales/gd.yml +99 -0
  75. data/web/locales/he.yml +65 -64
  76. data/web/locales/hi.yml +59 -59
  77. data/web/locales/it.yml +53 -53
  78. data/web/locales/ja.yml +73 -68
  79. data/web/locales/ko.yml +52 -52
  80. data/web/locales/lt.yml +66 -66
  81. data/web/locales/nb.yml +61 -61
  82. data/web/locales/nl.yml +52 -52
  83. data/web/locales/pl.yml +45 -45
  84. data/web/locales/pt-br.yml +63 -55
  85. data/web/locales/pt.yml +51 -51
  86. data/web/locales/ru.yml +67 -66
  87. data/web/locales/sv.yml +53 -53
  88. data/web/locales/ta.yml +60 -60
  89. data/web/locales/uk.yml +62 -61
  90. data/web/locales/ur.yml +64 -64
  91. data/web/locales/vi.yml +67 -67
  92. data/web/locales/zh-cn.yml +43 -16
  93. data/web/locales/zh-tw.yml +42 -8
  94. data/web/views/_footer.erb +6 -3
  95. data/web/views/_job_info.erb +18 -2
  96. data/web/views/_metrics_period_select.erb +12 -0
  97. data/web/views/_nav.erb +1 -1
  98. data/web/views/_paging.erb +2 -0
  99. data/web/views/_poll_link.erb +3 -6
  100. data/web/views/_summary.erb +7 -7
  101. data/web/views/busy.erb +44 -28
  102. data/web/views/dashboard.erb +44 -12
  103. data/web/views/layout.erb +1 -1
  104. data/web/views/metrics.erb +82 -0
  105. data/web/views/metrics_for_job.erb +68 -0
  106. data/web/views/morgue.erb +5 -9
  107. data/web/views/queue.erb +24 -24
  108. data/web/views/queues.erb +4 -2
  109. data/web/views/retries.erb +5 -9
  110. data/web/views/scheduled.erb +12 -13
  111. metadata +62 -31
  112. data/LICENSE +0 -9
  113. data/lib/generators/sidekiq/worker_generator.rb +0 -57
  114. data/lib/sidekiq/delay.rb +0 -41
  115. data/lib/sidekiq/exception_handler.rb +0 -27
  116. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  117. data/lib/sidekiq/extensions/active_record.rb +0 -43
  118. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  119. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  120. data/lib/sidekiq/worker.rb +0 -244
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/scheduled"
4
- require "sidekiq/api"
5
-
6
3
  require "zlib"
7
4
  require "base64"
5
+ require "sidekiq/component"
8
6
 
9
7
  module Sidekiq
10
8
  ##
@@ -25,18 +23,19 @@ module Sidekiq
25
23
  #
26
24
  # A job looks like:
27
25
  #
28
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => true }
26
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => true }
29
27
  #
30
28
  # The 'retry' option also accepts a number (in place of 'true'):
31
29
  #
32
- # { 'class' => 'HardWorker', 'args' => [1, 2, 'foo'], 'retry' => 5 }
30
+ # { 'class' => 'HardJob', 'args' => [1, 2, 'foo'], 'retry' => 5 }
33
31
  #
34
32
  # The job will be retried this number of times before giving up. (If simply
35
33
  # 'true', Sidekiq retries 25 times)
36
34
  #
37
- # We'll add a bit more data to the job to support retries:
35
+ # Relevant options for job retries:
38
36
  #
39
- # * '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
40
39
  # * 'retry_count' - number of times we've retried so far.
41
40
  # * 'error_message' - the message from the exception
42
41
  # * 'error_class' - the exception class
@@ -50,13 +49,14 @@ module Sidekiq
50
49
  # The default number of retries is 25 which works out to about 3 weeks
51
50
  # You can change the default maximum number of retries in your initializer:
52
51
  #
53
- # Sidekiq.options[:max_retries] = 7
52
+ # Sidekiq.default_configuration[:max_retries] = 7
54
53
  #
55
- # 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:
56
56
  #
57
- # class MyWorker
58
- # include Sidekiq::Worker
59
- # sidekiq_options :retry => 10
57
+ # class MyJob
58
+ # include Sidekiq::Job
59
+ # sidekiq_options retry: 10, retry_queue: 'low'
60
60
  # end
61
61
  #
62
62
  class JobRetry
@@ -64,17 +64,19 @@ module Sidekiq
64
64
 
65
65
  class Skip < Handled; end
66
66
 
67
- include Sidekiq::Util
67
+ include Sidekiq::Component
68
68
 
69
69
  DEFAULT_MAX_RETRY_ATTEMPTS = 25
70
70
 
71
- def initialize(options = {})
72
- @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]
73
75
  end
74
76
 
75
77
  # The global retry handler requires only the barest of data.
76
78
  # We want to be able to retry as much as possible so we don't
77
- # require the worker to be instantiated.
79
+ # require the job to be instantiated.
78
80
  def global(jobstr, queue)
79
81
  yield
80
82
  rescue Handled => ex
@@ -88,9 +90,9 @@ module Sidekiq
88
90
 
89
91
  msg = Sidekiq.load_json(jobstr)
90
92
  if msg["retry"]
91
- attempt_retry(nil, msg, queue, e)
93
+ process_retry(nil, msg, queue, e)
92
94
  else
93
- Sidekiq.death_handlers.each do |handler|
95
+ @capsule.config.death_handlers.each do |handler|
94
96
  handler.call(msg, e)
95
97
  rescue => handler_ex
96
98
  handle_exception(handler_ex, {context: "Error calling death handler", job: msg})
@@ -101,14 +103,14 @@ module Sidekiq
101
103
  end
102
104
 
103
105
  # The local retry support means that any errors that occur within
104
- # this block can be associated with the given worker instance.
106
+ # this block can be associated with the given job instance.
105
107
  # This is required to support the `sidekiq_retries_exhausted` block.
106
108
  #
107
109
  # Note that any exception from the block is wrapped in the Skip
108
110
  # exception so the global block does not reprocess the error. The
109
111
  # Skip exception is unwrapped within Sidekiq::Processor#process before
110
112
  # calling the handle_exception handlers.
111
- def local(worker, jobstr, queue)
113
+ def local(jobinst, jobstr, queue)
112
114
  yield
113
115
  rescue Handled => ex
114
116
  raise ex
@@ -121,11 +123,11 @@ module Sidekiq
121
123
 
122
124
  msg = Sidekiq.load_json(jobstr)
123
125
  if msg["retry"].nil?
124
- msg["retry"] = worker.class.get_sidekiq_options["retry"]
126
+ msg["retry"] = jobinst.class.get_sidekiq_options["retry"]
125
127
  end
126
128
 
127
129
  raise e unless msg["retry"]
128
- attempt_retry(worker, msg, queue, e)
130
+ process_retry(jobinst, msg, queue, e)
129
131
  # We've handled this error associated with this job, don't
130
132
  # need to handle it at the global level
131
133
  raise Skip
@@ -133,10 +135,10 @@ module Sidekiq
133
135
 
134
136
  private
135
137
 
136
- # Note that +worker+ can be nil here if an error is raised before we can
137
- # 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
138
140
  # best effort.
139
- def attempt_retry(worker, msg, queue, exception)
141
+ def process_retry(jobinst, msg, queue, exception)
140
142
  max_retry_attempts = retry_attempts_from(msg["retry"], @max_retries)
141
143
 
142
144
  msg["queue"] = (msg["retry_queue"] || queue)
@@ -158,33 +160,65 @@ module Sidekiq
158
160
  end
159
161
 
160
162
  if msg["backtrace"]
163
+ backtrace = @backtrace_cleaner.call(exception.backtrace)
161
164
  lines = if msg["backtrace"] == true
162
- exception.backtrace
165
+ backtrace
163
166
  else
164
- exception.backtrace[0...msg["backtrace"].to_i]
167
+ backtrace[0...msg["backtrace"].to_i]
165
168
  end
166
169
 
167
170
  msg["error_backtrace"] = compress_backtrace(lines)
168
171
  end
169
172
 
170
- if count < max_retry_attempts
171
- delay = delay_for(worker, count, exception)
172
- # Logging here can break retries if the logging device raises ENOSPC #3979
173
- # logger.debug { "Failure! Retry #{count} in #{delay} seconds" }
174
- retry_at = Time.now.to_f + delay
175
- payload = Sidekiq.dump_json(msg)
176
- Sidekiq.redis do |conn|
177
- conn.zadd("retry", retry_at.to_s, payload)
178
- end
179
- else
180
- # Goodbye dear message, you (re)tried your best I'm sure.
181
- 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)
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]
182
214
  end
215
+
216
+ [:default, delay]
183
217
  end
184
218
 
185
- def retries_exhausted(worker, msg, exception)
219
+ def retries_exhausted(jobinst, msg, exception)
186
220
  begin
187
- block = worker&.sidekiq_retries_exhausted_block
221
+ block = jobinst&.sidekiq_retries_exhausted_block
188
222
  block&.call(msg, exception)
189
223
  rescue => e
190
224
  handle_exception(e, {context: "Error calling retries_exhausted", job: msg})
@@ -192,7 +226,7 @@ module Sidekiq
192
226
 
193
227
  send_to_morgue(msg) unless msg["dead"] == false
194
228
 
195
- Sidekiq.death_handlers.each do |handler|
229
+ @capsule.config.death_handlers.each do |handler|
196
230
  handler.call(msg, exception)
197
231
  rescue => e
198
232
  handle_exception(e, {context: "Error calling death handler", job: msg})
@@ -202,7 +236,15 @@ module Sidekiq
202
236
  def send_to_morgue(msg)
203
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,22 +255,6 @@ module Sidekiq
213
255
  end
214
256
  end
215
257
 
216
- def delay_for(worker, count, exception)
217
- jitter = rand(10) * (count + 1)
218
- if worker&.sidekiq_retry_in_block
219
- custom_retry_in = retry_in(worker, count, exception).to_i
220
- return custom_retry_in + jitter if custom_retry_in > 0
221
- end
222
- (count**4) + 15 + jitter
223
- end
224
-
225
- def retry_in(worker, count, exception)
226
- worker.sidekiq_retry_in_block.call(count, exception)
227
- rescue Exception => e
228
- handle_exception(e, {context: "Failure scheduling retry using the defined `sidekiq_retry_in` in #{worker.class.name}, falling back to default"})
229
- nil
230
- end
231
-
232
258
  def exception_caused_by_shutdown?(e, checked_causes = [])
233
259
  return false unless e.cause
234
260
 
@@ -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