sidekiq 6.0.7 → 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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +160 -2
  3. data/LICENSE +3 -3
  4. data/README.md +5 -9
  5. data/bin/sidekiq +7 -2
  6. data/lib/generators/sidekiq/job_generator.rb +57 -0
  7. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  8. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  9. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  10. data/lib/sidekiq/api.rb +104 -60
  11. data/lib/sidekiq/cli.rb +36 -10
  12. data/lib/sidekiq/client.rb +12 -45
  13. data/lib/sidekiq/delay.rb +2 -0
  14. data/lib/sidekiq/extensions/action_mailer.rb +5 -4
  15. data/lib/sidekiq/extensions/active_record.rb +6 -5
  16. data/lib/sidekiq/extensions/class_methods.rb +7 -6
  17. data/lib/sidekiq/extensions/generic_proxy.rb +5 -3
  18. data/lib/sidekiq/fetch.rb +30 -21
  19. data/lib/sidekiq/job.rb +13 -0
  20. data/lib/sidekiq/job_logger.rb +1 -1
  21. data/lib/sidekiq/job_retry.rb +10 -11
  22. data/lib/sidekiq/job_util.rb +65 -0
  23. data/lib/sidekiq/launcher.rb +79 -21
  24. data/lib/sidekiq/logger.rb +3 -2
  25. data/lib/sidekiq/manager.rb +10 -12
  26. data/lib/sidekiq/middleware/chain.rb +6 -4
  27. data/lib/sidekiq/middleware/current_attributes.rb +57 -0
  28. data/lib/sidekiq/processor.rb +4 -4
  29. data/lib/sidekiq/rails.rb +27 -18
  30. data/lib/sidekiq/redis_connection.rb +14 -13
  31. data/lib/sidekiq/scheduled.rb +51 -16
  32. data/lib/sidekiq/sd_notify.rb +1 -1
  33. data/lib/sidekiq/testing.rb +2 -4
  34. data/lib/sidekiq/util.rb +41 -0
  35. data/lib/sidekiq/version.rb +1 -1
  36. data/lib/sidekiq/web/action.rb +2 -2
  37. data/lib/sidekiq/web/application.rb +21 -12
  38. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  39. data/lib/sidekiq/web/helpers.rb +35 -29
  40. data/lib/sidekiq/web/router.rb +5 -2
  41. data/lib/sidekiq/web.rb +36 -72
  42. data/lib/sidekiq/worker.rb +129 -12
  43. data/lib/sidekiq.rb +12 -3
  44. data/sidekiq.gemspec +11 -4
  45. data/web/assets/images/apple-touch-icon.png +0 -0
  46. data/web/assets/javascripts/application.js +82 -66
  47. data/web/assets/javascripts/dashboard.js +51 -51
  48. data/web/assets/stylesheets/application-dark.css +64 -43
  49. data/web/assets/stylesheets/application-rtl.css +0 -4
  50. data/web/assets/stylesheets/application.css +41 -239
  51. data/web/locales/ar.yml +8 -2
  52. data/web/locales/en.yml +4 -1
  53. data/web/locales/es.yml +18 -2
  54. data/web/locales/fr.yml +8 -1
  55. data/web/locales/ja.yml +3 -0
  56. data/web/locales/lt.yml +1 -1
  57. data/web/locales/pl.yml +4 -4
  58. data/web/locales/ru.yml +4 -0
  59. data/web/views/_footer.erb +1 -1
  60. data/web/views/_job_info.erb +1 -1
  61. data/web/views/_poll_link.erb +2 -5
  62. data/web/views/_summary.erb +7 -7
  63. data/web/views/busy.erb +50 -19
  64. data/web/views/dashboard.erb +22 -14
  65. data/web/views/dead.erb +1 -1
  66. data/web/views/layout.erb +2 -1
  67. data/web/views/morgue.erb +6 -6
  68. data/web/views/queue.erb +11 -11
  69. data/web/views/queues.erb +4 -4
  70. data/web/views/retries.erb +7 -7
  71. data/web/views/retry.erb +1 -1
  72. data/web/views/scheduled.erb +1 -1
  73. metadata +24 -49
  74. data/.circleci/config.yml +0 -60
  75. data/.github/contributing.md +0 -32
  76. data/.github/issue_template.md +0 -11
  77. data/.gitignore +0 -13
  78. data/.standard.yml +0 -20
  79. data/3.0-Upgrade.md +0 -70
  80. data/4.0-Upgrade.md +0 -53
  81. data/5.0-Upgrade.md +0 -56
  82. data/6.0-Upgrade.md +0 -72
  83. data/COMM-LICENSE +0 -97
  84. data/Ent-2.0-Upgrade.md +0 -37
  85. data/Ent-Changes.md +0 -256
  86. data/Gemfile +0 -24
  87. data/Gemfile.lock +0 -208
  88. data/Pro-2.0-Upgrade.md +0 -138
  89. data/Pro-3.0-Upgrade.md +0 -44
  90. data/Pro-4.0-Upgrade.md +0 -35
  91. data/Pro-5.0-Upgrade.md +0 -25
  92. data/Pro-Changes.md +0 -782
  93. data/Rakefile +0 -10
  94. data/code_of_conduct.md +0 -50
  95. data/lib/generators/sidekiq/worker_generator.rb +0 -57
data/lib/sidekiq/cli.rb CHANGED
@@ -33,8 +33,9 @@ module Sidekiq
33
33
  # Code within this method is not tested because it alters
34
34
  # global process state irreversibly. PRs which improve the
35
35
  # test coverage of Sidekiq::CLI are welcomed.
36
- def run
37
- boot_system
36
+ def run(boot_app: true)
37
+ boot_application if boot_app
38
+
38
39
  if environment == "development" && $stdout.tty? && Sidekiq.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
39
40
  print_banner
40
41
  end
@@ -43,9 +44,17 @@ module Sidekiq
43
44
  self_read, self_write = IO.pipe
44
45
  sigs = %w[INT TERM TTIN TSTP]
45
46
  # USR1 and USR2 don't work on the JVM
46
- sigs << "USR2" unless jruby?
47
+ sigs << "USR2" if Sidekiq.pro? && !jruby?
47
48
  sigs.each do |sig|
48
- trap sig do
49
+ old_handler = Signal.trap(sig) do
50
+ if old_handler.respond_to?(:call)
51
+ begin
52
+ old_handler.call
53
+ rescue Exception => exc
54
+ # signal handlers can't use Logger so puts only
55
+ puts ["Error in #{sig} handler", exc].inspect
56
+ end
57
+ end
49
58
  self_write.puts(sig)
50
59
  end
51
60
  rescue ArgumentError
@@ -58,9 +67,22 @@ module Sidekiq
58
67
 
59
68
  # touch the connection pool so it is created before we
60
69
  # fire startup and start multithreading.
61
- ver = Sidekiq.redis_info["redis_version"]
70
+ info = Sidekiq.redis_info
71
+ ver = info["redis_version"]
62
72
  raise "You are connecting to Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
63
73
 
74
+ maxmemory_policy = info["maxmemory_policy"]
75
+ if maxmemory_policy != "noeviction"
76
+ logger.warn <<~EOM
77
+
78
+
79
+ WARNING: Your Redis instance will evict Sidekiq data under heavy load.
80
+ The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
81
+ See: https://github.com/mperham/sidekiq/wiki/Using-Redis#memory
82
+
83
+ EOM
84
+ end
85
+
64
86
  # Since the user can pass us a connection pool explicitly in the initializer, we
65
87
  # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
66
88
  cursize = Sidekiq.redis_pool.size
@@ -228,8 +250,7 @@ module Sidekiq
228
250
  opts = parse_config(opts[:config_file]).merge(opts) if opts[:config_file]
229
251
 
230
252
  # set defaults
231
- opts[:queues] = ["default"] if opts[:queues].nil? || opts[:queues].empty?
232
- opts[:strict] = true if opts[:strict].nil?
253
+ opts[:queues] = ["default"] if opts[:queues].nil?
233
254
  opts[:concurrency] = Integer(ENV["RAILS_MAX_THREADS"]) if opts[:concurrency].nil? && ENV["RAILS_MAX_THREADS"]
234
255
 
235
256
  # merge with defaults
@@ -240,7 +261,7 @@ module Sidekiq
240
261
  Sidekiq.options
241
262
  end
242
263
 
243
- def boot_system
264
+ def boot_application
244
265
  ENV["RACK_ENV"] = ENV["RAILS_ENV"] = environment
245
266
 
246
267
  if File.directory?(options[:require])
@@ -359,7 +380,9 @@ module Sidekiq
359
380
  end
360
381
 
361
382
  def parse_config(path)
362
- opts = YAML.load(ERB.new(File.read(path)).result) || {}
383
+ erb = ERB.new(File.read(path))
384
+ erb.filename = File.expand_path(path)
385
+ opts = YAML.load(erb.result) || {}
363
386
 
364
387
  if opts.respond_to? :deep_symbolize_keys!
365
388
  opts.deep_symbolize_keys!
@@ -368,6 +391,8 @@ module Sidekiq
368
391
  end
369
392
 
370
393
  opts = opts.merge(opts.delete(environment.to_sym) || {})
394
+ opts.delete(:strict)
395
+
371
396
  parse_queues(opts, opts.delete(:queues) || [])
372
397
 
373
398
  opts
@@ -379,8 +404,9 @@ module Sidekiq
379
404
 
380
405
  def parse_queue(opts, queue, weight = nil)
381
406
  opts[:queues] ||= []
407
+ opts[:strict] = true if opts[:strict].nil?
382
408
  raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
383
- [weight.to_i, 1].max.times { opts[:queues] << queue }
409
+ [weight.to_i, 1].max.times { opts[:queues] << queue.to_s }
384
410
  opts[:strict] = false if weight.to_i > 0
385
411
  end
386
412
 
@@ -2,9 +2,12 @@
2
2
 
3
3
  require "securerandom"
4
4
  require "sidekiq/middleware/chain"
5
+ require "sidekiq/job_util"
5
6
 
6
7
  module Sidekiq
7
8
  class Client
9
+ include Sidekiq::JobUtil
10
+
8
11
  ##
9
12
  # Define client-side middleware:
10
13
  #
@@ -19,7 +22,7 @@ module Sidekiq
19
22
  #
20
23
  def middleware(&block)
21
24
  @chain ||= Sidekiq.client_middleware
22
- if block_given?
25
+ if block
23
26
  @chain = @chain.dup
24
27
  yield @chain
25
28
  end
@@ -90,16 +93,17 @@ module Sidekiq
90
93
  # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
91
94
  # than the number given if the middleware stopped processing for one or more jobs.
92
95
  def push_bulk(items)
93
- arg = items["args"].first
94
- return [] unless arg # no jobs to push
95
- raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless arg.is_a?(Array)
96
+ args = items["args"]
97
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless args.is_a?(Array) && args.all?(Array)
98
+ return [] if args.empty? # no jobs to push
96
99
 
97
100
  at = items.delete("at")
98
- raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all?(Numeric))
101
+ raise ArgumentError, "Job 'at' must be a Numeric or an Array of Numeric timestamps" if at && (Array(at).empty? || !Array(at).all? { |entry| entry.is_a?(Numeric) })
102
+ raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
99
103
 
100
104
  normed = normalize_item(items)
101
- payloads = items["args"].map.with_index { |args, index|
102
- copy = normed.merge("args" => args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f)
105
+ payloads = args.map.with_index { |job_args, index|
106
+ copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12), "enqueued_at" => Time.now.to_f)
103
107
  copy["at"] = (at.is_a?(Array) ? at[index] : at) if at
104
108
 
105
109
  result = process_single(items["class"], copy)
@@ -185,7 +189,7 @@ module Sidekiq
185
189
 
186
190
  def raw_push(payloads)
187
191
  @redis_pool.with do |conn|
188
- conn.multi do
192
+ conn.pipelined do
189
193
  atomic_push(conn, payloads)
190
194
  end
191
195
  end
@@ -217,42 +221,5 @@ module Sidekiq
217
221
  item
218
222
  end
219
223
  end
220
-
221
- def normalize_item(item)
222
- # 6.0.0 push_bulk bug, #4321
223
- # TODO Remove after a while...
224
- item.delete("at") if item.key?("at") && item["at"].nil?
225
-
226
- raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: { 'class' => SomeWorker, 'args' => ['bob', 1, :foo => 'bar'] }") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
227
- raise(ArgumentError, "Job args must be an Array") unless item["args"].is_a?(Array)
228
- raise(ArgumentError, "Job class must be either a Class or String representation of the class name") unless item["class"].is_a?(Class) || item["class"].is_a?(String)
229
- raise(ArgumentError, "Job 'at' must be a Numeric timestamp") if item.key?("at") && !item["at"].is_a?(Numeric)
230
- raise(ArgumentError, "Job tags must be an Array") if item["tags"] && !item["tags"].is_a?(Array)
231
- # raise(ArgumentError, "Arguments must be native JSON types, see https://github.com/mperham/sidekiq/wiki/Best-Practices") unless JSON.load(JSON.dump(item['args'])) == item['args']
232
-
233
- # merge in the default sidekiq_options for the item's class and/or wrapped element
234
- # this allows ActiveJobs to control sidekiq_options too.
235
- defaults = normalized_hash(item["class"])
236
- defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?("get_sidekiq_options")
237
- item = defaults.merge(item)
238
-
239
- raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
240
-
241
- item["class"] = item["class"].to_s
242
- item["queue"] = item["queue"].to_s
243
- item["jid"] ||= SecureRandom.hex(12)
244
- item["created_at"] ||= Time.now.to_f
245
-
246
- item
247
- end
248
-
249
- def normalized_hash(item_class)
250
- if item_class.is_a?(Class)
251
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?("get_sidekiq_options")
252
- item_class.get_sidekiq_options
253
- else
254
- Sidekiq.default_worker_options
255
- end
256
- end
257
224
  end
258
225
  end
data/lib/sidekiq/delay.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  module Sidekiq
4
4
  module Extensions
5
5
  def self.enable_delay!
6
+ Sidekiq.logger.error "Sidekiq's Delayed Extensions will be removed in Sidekiq 7.0. #{caller(1..1).first}"
7
+
6
8
  if defined?(::ActiveSupport)
7
9
  require "sidekiq/extensions/active_record"
8
10
  require "sidekiq/extensions/action_mailer"
@@ -5,9 +5,10 @@ require "sidekiq/extensions/generic_proxy"
5
5
  module Sidekiq
6
6
  module Extensions
7
7
  ##
8
- # Adds 'delay', 'delay_for' and `delay_until` methods to ActionMailer to offload arbitrary email
9
- # delivery to Sidekiq. Example:
8
+ # Adds +delay+, +delay_for+ and +delay_until+ methods to ActionMailer to offload arbitrary email
9
+ # delivery to Sidekiq.
10
10
  #
11
+ # @example
11
12
  # UserMailer.delay.send_welcome_email(new_user)
12
13
  # UserMailer.delay_for(5.days).send_welcome_email(new_user)
13
14
  # UserMailer.delay_until(5.days.from_now).send_welcome_email(new_user)
@@ -15,8 +16,8 @@ module Sidekiq
15
16
  include Sidekiq::Worker
16
17
 
17
18
  def perform(yml)
18
- (target, method_name, args) = YAML.load(yml)
19
- msg = target.public_send(method_name, *args)
19
+ (target, method_name, args, kwargs) = YAML.load(yml)
20
+ msg = kwargs.empty? ? target.public_send(method_name, *args) : target.public_send(method_name, *args, **kwargs)
20
21
  # The email method can return nil, which causes ActionMailer to return
21
22
  # an undeliverable empty message.
22
23
  if msg
@@ -5,10 +5,11 @@ require "sidekiq/extensions/generic_proxy"
5
5
  module Sidekiq
6
6
  module Extensions
7
7
  ##
8
- # Adds 'delay', 'delay_for' and `delay_until` methods to ActiveRecord to offload instance method
9
- # execution to Sidekiq. Examples:
8
+ # Adds +delay+, +delay_for+ and +delay_until+ methods to ActiveRecord to offload instance method
9
+ # execution to Sidekiq.
10
10
  #
11
- # User.recent_signups.each { |user| user.delay.mark_as_awesome }
11
+ # @example
12
+ # User.recent_signups.each { |user| user.delay.mark_as_awesome }
12
13
  #
13
14
  # Please note, this is not recommended as this will serialize the entire
14
15
  # object to Redis. Your Sidekiq jobs should pass IDs, not entire instances.
@@ -17,8 +18,8 @@ module Sidekiq
17
18
  include Sidekiq::Worker
18
19
 
19
20
  def perform(yml)
20
- (target, method_name, args) = YAML.load(yml)
21
- target.__send__(method_name, *args)
21
+ (target, method_name, args, kwargs) = YAML.load(yml)
22
+ kwargs.empty? ? target.__send__(method_name, *args) : target.__send__(method_name, *args, **kwargs)
22
23
  end
23
24
  end
24
25
 
@@ -5,18 +5,19 @@ require "sidekiq/extensions/generic_proxy"
5
5
  module Sidekiq
6
6
  module Extensions
7
7
  ##
8
- # Adds 'delay', 'delay_for' and `delay_until` methods to all Classes to offload class method
9
- # execution to Sidekiq. Examples:
8
+ # Adds `delay`, `delay_for` and `delay_until` methods to all Classes to offload class method
9
+ # execution to Sidekiq.
10
10
  #
11
- # User.delay.delete_inactive
12
- # Wikipedia.delay.download_changes_for(Date.today)
11
+ # @example
12
+ # User.delay.delete_inactive
13
+ # Wikipedia.delay.download_changes_for(Date.today)
13
14
  #
14
15
  class DelayedClass
15
16
  include Sidekiq::Worker
16
17
 
17
18
  def perform(yml)
18
- (target, method_name, args) = YAML.load(yml)
19
- target.__send__(method_name, *args)
19
+ (target, method_name, args, kwargs) = YAML.load(yml)
20
+ kwargs.empty? ? target.__send__(method_name, *args) : target.__send__(method_name, *args, **kwargs)
20
21
  end
21
22
  end
22
23
 
@@ -13,18 +13,20 @@ module Sidekiq
13
13
  @opts = options
14
14
  end
15
15
 
16
- def method_missing(name, *args)
16
+ def method_missing(name, *args, **kwargs)
17
17
  # Sidekiq has a limitation in that its message must be JSON.
18
18
  # JSON can't round trip real Ruby objects so we use YAML to
19
19
  # serialize the objects to a String. The YAML will be converted
20
20
  # to JSON and then deserialized on the other side back into a
21
21
  # Ruby object.
22
- obj = [@target, name, args]
22
+ obj = [@target, name, args, kwargs]
23
23
  marshalled = ::YAML.dump(obj)
24
24
  if marshalled.size > SIZE_LIMIT
25
25
  ::Sidekiq.logger.warn { "#{@target}.#{name} job argument is #{marshalled.bytesize} bytes, you should refactor it to reduce the size" }
26
26
  end
27
- @performable.client_push({"class" => @performable, "args" => [marshalled]}.merge(@opts))
27
+ @performable.client_push({"class" => @performable,
28
+ "args" => [marshalled],
29
+ "display_class" => "#{@target}.#{name}"}.merge(@opts))
28
30
  end
29
31
  end
30
32
  end
data/lib/sidekiq/fetch.rb CHANGED
@@ -25,8 +25,10 @@ module Sidekiq
25
25
  }
26
26
 
27
27
  def initialize(options)
28
- @strictly_ordered_queues = !!options[:strict]
29
- @queues = options[:queues].map { |q| "queue:#{q}" }
28
+ raise ArgumentError, "missing queue list" unless options[:queues]
29
+ @options = options
30
+ @strictly_ordered_queues = !!@options[:strict]
31
+ @queues = @options[:queues].map { |q| "queue:#{q}" }
30
32
  if @strictly_ordered_queues
31
33
  @queues.uniq!
32
34
  @queues << TIMEOUT
@@ -34,28 +36,19 @@ module Sidekiq
34
36
  end
35
37
 
36
38
  def retrieve_work
37
- work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd) }
38
- UnitOfWork.new(*work) if work
39
- end
40
-
41
- # Creating the Redis#brpop command takes into account any
42
- # configured queue weights. By default Redis#brpop returns
43
- # data from the first queue that has pending elements. We
44
- # recreate the queue command each time we invoke Redis#brpop
45
- # to honor weights and avoid queue starvation.
46
- def queues_cmd
47
- if @strictly_ordered_queues
48
- @queues
49
- else
50
- queues = @queues.shuffle!.uniq
51
- queues << TIMEOUT
52
- queues
39
+ qs = queues_cmd
40
+ # 4825 Sidekiq Pro with all queues paused will return an
41
+ # empty set of queues with a trailing TIMEOUT value.
42
+ if qs.size <= 1
43
+ sleep(TIMEOUT)
44
+ return nil
53
45
  end
46
+
47
+ work = Sidekiq.redis { |conn| conn.brpop(*qs) }
48
+ UnitOfWork.new(*work) if work
54
49
  end
55
50
 
56
- # By leaving this as a class method, it can be pluggable and used by the Manager actor. Making it
57
- # an instance method will make it async to the Fetcher actor
58
- def self.bulk_requeue(inprogress, options)
51
+ def bulk_requeue(inprogress, options)
59
52
  return if inprogress.empty?
60
53
 
61
54
  Sidekiq.logger.debug { "Re-queueing terminated jobs" }
@@ -76,5 +69,21 @@ module Sidekiq
76
69
  rescue => ex
77
70
  Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
78
71
  end
72
+
73
+ # Creating the Redis#brpop command takes into account any
74
+ # configured queue weights. By default Redis#brpop returns
75
+ # data from the first queue that has pending elements. We
76
+ # recreate the queue command each time we invoke Redis#brpop
77
+ # to honor weights and avoid queue starvation.
78
+ def queues_cmd
79
+ if @strictly_ordered_queues
80
+ @queues
81
+ else
82
+ permute = @queues.shuffle
83
+ permute.uniq!
84
+ permute << TIMEOUT
85
+ permute
86
+ end
87
+ end
79
88
  end
80
89
  end
@@ -0,0 +1,13 @@
1
+ require "sidekiq/worker"
2
+
3
+ module Sidekiq
4
+ # Sidekiq::Job is a new alias for Sidekiq::Worker as of Sidekiq 6.3.0.
5
+ # Use `include Sidekiq::Job` rather than `include Sidekiq::Worker`.
6
+ #
7
+ # The term "worker" is too generic and overly confusing, used in several
8
+ # different contexts meaning different things. Many people call a Sidekiq
9
+ # process a "worker". Some people call the thread that executes jobs a
10
+ # "worker". This change brings Sidekiq closer to ActiveJob where your job
11
+ # classes extend ApplicationJob.
12
+ Job = Worker
13
+ end
@@ -38,7 +38,7 @@ module Sidekiq
38
38
  # If we're using a wrapper class, like ActiveJob, use the "wrapped"
39
39
  # attribute to expose the underlying thing.
40
40
  h = {
41
- class: job_hash["wrapped"] || job_hash["class"],
41
+ class: job_hash["display_class"] || job_hash["wrapped"] || job_hash["class"],
42
42
  jid: job_hash["jid"]
43
43
  }
44
44
  h[:bid] = job_hash["bid"] if job_hash["bid"]
@@ -34,9 +34,10 @@ module Sidekiq
34
34
  # The job will be retried this number of times before giving up. (If simply
35
35
  # 'true', Sidekiq retries 25 times)
36
36
  #
37
- # We'll add a bit more data to the job to support retries:
37
+ # Relevant options for job retries:
38
38
  #
39
- # * '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
40
41
  # * 'retry_count' - number of times we've retried so far.
41
42
  # * 'error_message' - the message from the exception
42
43
  # * 'error_class' - the exception class
@@ -52,15 +53,17 @@ module Sidekiq
52
53
  #
53
54
  # Sidekiq.options[:max_retries] = 7
54
55
  #
55
- # 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:
56
58
  #
57
59
  # class MyWorker
58
60
  # include Sidekiq::Worker
59
- # sidekiq_options :retry => 10
61
+ # sidekiq_options retry: 10, retry_queue: 'low'
60
62
  # end
61
63
  #
62
64
  class JobRetry
63
65
  class Handled < ::RuntimeError; end
66
+
64
67
  class Skip < Handled; end
65
68
 
66
69
  include Sidekiq::Util
@@ -213,16 +216,12 @@ module Sidekiq
213
216
  end
214
217
 
215
218
  def delay_for(worker, count, exception)
219
+ jitter = rand(10) * (count + 1)
216
220
  if worker&.sidekiq_retry_in_block
217
221
  custom_retry_in = retry_in(worker, count, exception).to_i
218
- return custom_retry_in if custom_retry_in > 0
222
+ return custom_retry_in + jitter if custom_retry_in > 0
219
223
  end
220
- seconds_to_delay(count)
221
- end
222
-
223
- # delayed_job uses the same basic formula
224
- def seconds_to_delay(count)
225
- (count**4) + 15 + (rand(30) * (count + 1))
224
+ (count**4) + 15 + jitter
226
225
  end
227
226
 
228
227
  def retry_in(worker, count, exception)
@@ -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