sidekiq 6.2.2 → 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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +69 -1
  3. data/LICENSE +3 -3
  4. data/README.md +3 -3
  5. data/lib/generators/sidekiq/job_generator.rb +57 -0
  6. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  7. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  8. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  9. data/lib/sidekiq/api.rb +7 -4
  10. data/lib/sidekiq/cli.rb +13 -3
  11. data/lib/sidekiq/client.rb +5 -39
  12. data/lib/sidekiq/delay.rb +2 -0
  13. data/lib/sidekiq/extensions/action_mailer.rb +2 -2
  14. data/lib/sidekiq/extensions/active_record.rb +2 -2
  15. data/lib/sidekiq/extensions/class_methods.rb +2 -2
  16. data/lib/sidekiq/extensions/generic_proxy.rb +2 -2
  17. data/lib/sidekiq/fetch.rb +4 -3
  18. data/lib/sidekiq/job.rb +8 -3
  19. data/lib/sidekiq/job_retry.rb +6 -4
  20. data/lib/sidekiq/job_util.rb +65 -0
  21. data/lib/sidekiq/launcher.rb +5 -1
  22. data/lib/sidekiq/manager.rb +7 -9
  23. data/lib/sidekiq/middleware/current_attributes.rb +57 -0
  24. data/lib/sidekiq/rails.rb +11 -0
  25. data/lib/sidekiq/redis_connection.rb +4 -6
  26. data/lib/sidekiq/scheduled.rb +44 -15
  27. data/lib/sidekiq/util.rb +13 -0
  28. data/lib/sidekiq/version.rb +1 -1
  29. data/lib/sidekiq/web/application.rb +7 -4
  30. data/lib/sidekiq/web/helpers.rb +1 -12
  31. data/lib/sidekiq/worker.rb +127 -7
  32. data/lib/sidekiq.rb +8 -1
  33. data/sidekiq.gemspec +1 -1
  34. data/web/assets/javascripts/application.js +82 -61
  35. data/web/assets/javascripts/dashboard.js +51 -51
  36. data/web/assets/stylesheets/application-dark.css +19 -23
  37. data/web/assets/stylesheets/application-rtl.css +0 -4
  38. data/web/assets/stylesheets/application.css +10 -108
  39. data/web/locales/en.yml +1 -1
  40. data/web/views/_footer.erb +1 -1
  41. data/web/views/_poll_link.erb +2 -5
  42. data/web/views/_summary.erb +7 -7
  43. data/web/views/dashboard.erb +8 -8
  44. data/web/views/layout.erb +1 -1
  45. data/web/views/queue.erb +10 -10
  46. data/web/views/queues.erb +1 -1
  47. metadata +9 -7
  48. data/lib/generators/sidekiq/worker_generator.rb +0 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b0da541124d097e05936cc860a93009ba170d714cbd8dbc760c1245818e8ed7
4
- data.tar.gz: 63995ccbb57fa16e4954cda4ab9610506b0a8ff35084492b520fc18608face35
3
+ metadata.gz: 9622b2851203b0c5a80695ab7801ca77e15dc63a641ae79132cda9a2fcbe0cc6
4
+ data.tar.gz: dd943a02d2cf910f51866d02254f3a7845cb159735bd54d223dd7127a1fbd2fa
5
5
  SHA512:
6
- metadata.gz: f3b864db74530840437c0af43475ed0e12a876d6d2cbbd808ecc72722adddbd8d5548c9b777d4e34af5deddf94871567717518f70d787ea7f6a089f2f5b2f4ed
7
- data.tar.gz: 4569eed5baa10134e99a0b1404c213e3b35c08ec158e2fa8b006918298750919e8d34074aa10d3b4dfb621bb18cbe68be467d1e7822fb855012cff0f64dcdc7b
6
+ metadata.gz: 65bcd542866d8699ecf5958d81a15fd322cd1c51dfc1dbc6a8b15402b70862510c134e2b0ed9f9dbcdbb4dea3a59c1d12878ff926145e503079a59896f279ff3
7
+ data.tar.gz: 36143e85dc7fd4611f8a43fe39788dc590725ac63a827fbc174916da2d59fdadb3fb64fa0d819c4ef90694f6f292ea15f7fb58fb29f368c0a06c5b90f1b4bca7
data/Changes.md CHANGED
@@ -1,6 +1,74 @@
1
1
  # Sidekiq Changes
2
2
 
3
- [Sidekiq Changes](https://github.com/mperham/sidekiq/blob/master/Changes.md) | [Sidekiq Pro Changes](https://github.com/mperham/sidekiq/blob/master/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/mperham/sidekiq/blob/master/Ent-Changes.md)
3
+ [Sidekiq Changes](https://github.com/mperham/sidekiq/blob/main/Changes.md) | [Sidekiq Pro Changes](https://github.com/mperham/sidekiq/blob/main/Pro-Changes.md) | [Sidekiq Enterprise Changes](https://github.com/mperham/sidekiq/blob/main/Ent-Changes.md)
4
+
5
+ 6.4.0
6
+ ---------
7
+
8
+ - **SECURITY**: Validate input to avoid possible DoS in Web UI.
9
+ - Add **strict argument checking** [#5071]
10
+ Sidekiq will now log a warning if JSON-unsafe arguments are passed to `perform_async`.
11
+ Add `Sidekiq.strict_args!(false)` to your initializer to disable this warning.
12
+ This warning will switch to an exception in Sidekiq 7.0.
13
+ - Note that Delayed Extensions will be removed in Sidekiq 7.0 [#5076]
14
+ - Add `perform_{inline,sync}` in Sidekiq::Job to run a job synchronously [#5061, hasan-ally]
15
+ ```ruby
16
+ SomeJob.perform_async(args...)
17
+ SomeJob.perform_sync(args...)
18
+ SomeJob.perform_inline(args...)
19
+ ```
20
+ You can also dynamically redirect a job to run synchronously:
21
+ ```ruby
22
+ SomeJob.set("sync": true).perform_async(args...) # will run via perform_inline
23
+ ```
24
+ - Replace Sidekiq::Worker `app/workers` generator with Sidekiq::Job `app/sidekiq` generator [#5055]
25
+ ```
26
+ bin/rails generate sidekiq:job ProcessOrderJob
27
+ ```
28
+ - Fix job retries losing CurrentAttributes [#5090]
29
+ - Tweak shutdown to give long-running threads time to cleanup [#5095]
30
+ - Add keyword arguments support in extensions
31
+
32
+ 6.3.1
33
+ ---------
34
+
35
+ - Fix keyword arguments error with CurrentAttributes on Ruby 3.0 [#5048]
36
+
37
+ 6.3.0
38
+ ---------
39
+
40
+ - **BREAK**: The Web UI has been refactored to remove jQuery. Any UI extensions
41
+ which use jQuery will break.
42
+ - **FEATURE**: Sidekiq.logger has been enhanced so any `Rails.logger`
43
+ output in jobs now shows up in the Sidekiq console. Remove any logger
44
+ hacks in your initializer and see if it Just Works™ now. [#5021]
45
+ - **FEATURE**: Add `Sidekiq::Job` alias for `Sidekiq::Worker`, to better
46
+ reflect industry standard terminology. You can now do this:
47
+ ```ruby
48
+ class MyJob
49
+ include Sidekiq::Job
50
+ sidekiq_options ...
51
+ def perform(args)
52
+ end
53
+ end
54
+ ```
55
+ - **FEATURE**: Support for serializing ActiveSupport::CurrentAttributes into each job. [#4982]
56
+ ```ruby
57
+ # config/initializers/sidekiq.rb
58
+ require "sidekiq/middleware/current_attributes"
59
+ Sidekiq::CurrentAttributes.persist(Myapp::Current) # Your AS::CurrentAttributes singleton
60
+ ```
61
+ - **FEATURE**: Add `Sidekiq::Worker.perform_bulk` for enqueuing jobs in bulk,
62
+ similar to `Sidekiq::Client.push_bulk` [#5042]
63
+ ```ruby
64
+ MyJob.perform_bulk([[1], [2], [3]])
65
+ ```
66
+ - Implement `queue_as`, `wait` and `wait_until` for ActiveJob compatibility [#5003]
67
+ - Scheduler now uses Lua to reduce Redis load and network roundtrips [#5044]
68
+ - Retry Redis operation if we get an `UNBLOCKED` Redis error [#4985]
69
+ - Run existing signal traps, if any, before running Sidekiq's trap [#4991]
70
+ - Fix fetch bug when using weighted queues which caused Sidekiq to stop
71
+ processing queues randomly [#5031]
4
72
 
5
73
  6.2.2
6
74
  ---------
data/LICENSE CHANGED
@@ -4,6 +4,6 @@ Sidekiq is an Open Source project licensed under the terms of
4
4
  the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
5
5
  for license text.
6
6
 
7
- Sidekiq Pro has a commercial-friendly license allowing private forks
8
- and modifications of Sidekiq. Please see https://sidekiq.org/products/pro.html for
9
- more detail. You can find the commercial license terms in COMM-LICENSE.txt.
7
+ Sidekiq Pro and Sidekiq Enterprise have a commercial-friendly license.
8
+ You can find the commercial license in COMM-LICENSE.txt.
9
+ Please see https://sidekiq.org for purchasing options.
data/README.md CHANGED
@@ -43,10 +43,10 @@ Getting Started
43
43
  -----------------
44
44
 
45
45
  See the [Getting Started wiki page](https://github.com/mperham/sidekiq/wiki/Getting-Started) and follow the simple setup process.
46
- You can watch [this Youtube playlist](https://www.youtube.com/playlist?list=PLjeHh2LSCFrWGT5uVjUuFKAcrcj5kSai1) to learn all about
46
+ You can watch [this YouTube playlist](https://www.youtube.com/playlist?list=PLjeHh2LSCFrWGT5uVjUuFKAcrcj5kSai1) to learn all about
47
47
  Sidekiq and see its features in action. Here's the Web UI:
48
48
 
49
- ![Web UI](https://github.com/mperham/sidekiq/raw/master/examples/web-ui.png)
49
+ ![Web UI](https://github.com/mperham/sidekiq/raw/main/examples/web-ui.png)
50
50
 
51
51
 
52
52
  Want to Upgrade?
@@ -84,7 +84,7 @@ See the [Sidekiq support page](https://sidekiq.org/support.html) for details.
84
84
  License
85
85
  -----------------
86
86
 
87
- Please see [LICENSE](https://github.com/mperham/sidekiq/blob/master/LICENSE) for licensing details.
87
+ Please see [LICENSE](https://github.com/mperham/sidekiq/blob/main/LICENSE) for licensing details.
88
88
 
89
89
 
90
90
  Author
@@ -0,0 +1,57 @@
1
+ require "rails/generators/named_base"
2
+
3
+ module Sidekiq
4
+ module Generators # :nodoc:
5
+ class JobGenerator < ::Rails::Generators::NamedBase # :nodoc:
6
+ desc "This generator creates a Sidekiq Job in app/sidekiq and a corresponding test"
7
+
8
+ check_class_collision suffix: "Job"
9
+
10
+ def self.default_generator_root
11
+ File.dirname(__FILE__)
12
+ end
13
+
14
+ def create_job_file
15
+ template "job.rb.erb", File.join("app/sidekiq", class_path, "#{file_name}_job.rb")
16
+ end
17
+
18
+ def create_test_file
19
+ return unless test_framework
20
+
21
+ if test_framework == :rspec
22
+ create_job_spec
23
+ else
24
+ create_job_test
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def create_job_spec
31
+ template_file = File.join(
32
+ "spec/sidekiq",
33
+ class_path,
34
+ "#{file_name}_job_spec.rb"
35
+ )
36
+ template "job_spec.rb.erb", template_file
37
+ end
38
+
39
+ def create_job_test
40
+ template_file = File.join(
41
+ "test/sidekiq",
42
+ class_path,
43
+ "#{file_name}_job_test.rb"
44
+ )
45
+ template "job_test.rb.erb", template_file
46
+ end
47
+
48
+ def file_name
49
+ @_file_name ||= super.sub(/_?job\z/i, "")
50
+ end
51
+
52
+ def test_framework
53
+ ::Rails.application.config.generators.options[:rails][:test_framework]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,6 +1,6 @@
1
1
  <% module_namespacing do -%>
2
- class <%= class_name %>Worker
3
- include Sidekiq::Worker
2
+ class <%= class_name %>Job
3
+ include Sidekiq::Job
4
4
 
5
5
  def perform(*args)
6
6
  # Do something
@@ -1,6 +1,6 @@
1
1
  require 'rails_helper'
2
2
  <% module_namespacing do -%>
3
- RSpec.describe <%= class_name %>Worker, type: :worker do
3
+ RSpec.describe <%= class_name %>Job, type: :job do
4
4
  pending "add some examples to (or delete) #{__FILE__}"
5
5
  end
6
6
  <% end -%>
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
  <% module_namespacing do -%>
3
- class <%= class_name %>WorkerTest < Minitest::Test
3
+ class <%= class_name %>JobTest < Minitest::Test
4
4
  def test_example
5
5
  skip "add some examples to (or delete) #{__FILE__}"
6
6
  end
data/lib/sidekiq/api.rb CHANGED
@@ -113,6 +113,7 @@ module Sidekiq
113
113
 
114
114
  @stats[:workers_size] = workers_size
115
115
  @stats[:enqueued] = enqueued
116
+ @stats
116
117
  end
117
118
 
118
119
  def fetch_stats!
@@ -160,6 +161,8 @@ module Sidekiq
160
161
 
161
162
  class History
162
163
  def initialize(days_previous, start_date = nil)
164
+ # we only store five years of data in Redis
165
+ raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
163
166
  @days_previous = days_previous
164
167
  @start_date = start_date || Time.now.utc.to_date
165
168
  end
@@ -818,10 +821,10 @@ module Sidekiq
818
821
 
819
822
  hash = Sidekiq.load_json(info)
820
823
  yield Process.new(hash.merge("busy" => busy.to_i,
821
- "beat" => at_s.to_f,
822
- "quiet" => quiet,
823
- "rss" => rss.to_i,
824
- "rtt_us" => rtt.to_i))
824
+ "beat" => at_s.to_f,
825
+ "quiet" => quiet,
826
+ "rss" => rss.to_i,
827
+ "rtt_us" => rtt.to_i))
825
828
  end
826
829
  end
827
830
 
data/lib/sidekiq/cli.rb CHANGED
@@ -46,7 +46,15 @@ module Sidekiq
46
46
  # USR1 and USR2 don't work on the JVM
47
47
  sigs << "USR2" if Sidekiq.pro? && !jruby?
48
48
  sigs.each do |sig|
49
- 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
50
58
  self_write.puts(sig)
51
59
  end
52
60
  rescue ArgumentError
@@ -372,7 +380,9 @@ module Sidekiq
372
380
  end
373
381
 
374
382
  def parse_config(path)
375
- 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) || {}
376
386
 
377
387
  if opts.respond_to? :deep_symbolize_keys!
378
388
  opts.deep_symbolize_keys!
@@ -396,7 +406,7 @@ module Sidekiq
396
406
  opts[:queues] ||= []
397
407
  opts[:strict] = true if opts[:strict].nil?
398
408
  raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
399
- [weight.to_i, 1].max.times { opts[:queues] << queue }
409
+ [weight.to_i, 1].max.times { opts[:queues] << queue.to_s }
400
410
  opts[:strict] = false if weight.to_i > 0
401
411
  end
402
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
  #
@@ -95,7 +98,7 @@ module Sidekiq
95
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) })
99
102
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
100
103
 
101
104
  normed = normalize_item(items)
@@ -186,7 +189,7 @@ module Sidekiq
186
189
 
187
190
  def raw_push(payloads)
188
191
  @redis_pool.with do |conn|
189
- conn.multi do
192
+ conn.pipelined do
190
193
  atomic_push(conn, payloads)
191
194
  end
192
195
  end
@@ -218,42 +221,5 @@ module Sidekiq
218
221
  item
219
222
  end
220
223
  end
221
-
222
- def validate(item)
223
- raise(ArgumentError, "Job must be a Hash with 'class' and 'args' keys: `#{item}`") unless item.is_a?(Hash) && item.key?("class") && item.key?("args")
224
- raise(ArgumentError, "Job args must be an Array: `#{item}`") unless item["args"].is_a?(Array)
225
- 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)
226
- raise(ArgumentError, "Job 'at' must be a Numeric timestamp: `#{item}`") if item.key?("at") && !item["at"].is_a?(Numeric)
227
- raise(ArgumentError, "Job tags must be an Array: `#{item}`") if item["tags"] && !item["tags"].is_a?(Array)
228
- end
229
-
230
- def normalize_item(item)
231
- validate(item)
232
- # 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']
233
-
234
- # merge in the default sidekiq_options for the item's class and/or wrapped element
235
- # this allows ActiveJobs to control sidekiq_options too.
236
- defaults = normalized_hash(item["class"])
237
- defaults = defaults.merge(item["wrapped"].get_sidekiq_options) if item["wrapped"].respond_to?("get_sidekiq_options")
238
- item = defaults.merge(item)
239
-
240
- raise(ArgumentError, "Job must include a valid queue name") if item["queue"].nil? || item["queue"] == ""
241
-
242
- item["class"] = item["class"].to_s
243
- item["queue"] = item["queue"].to_s
244
- item["jid"] ||= SecureRandom.hex(12)
245
- item["created_at"] ||= Time.now.to_f
246
-
247
- item
248
- end
249
-
250
- def normalized_hash(item_class)
251
- if item_class.is_a?(Class)
252
- raise(ArgumentError, "Message must include a Sidekiq::Worker class, not class name: #{item_class.ancestors.inspect}") unless item_class.respond_to?("get_sidekiq_options")
253
- item_class.get_sidekiq_options
254
- else
255
- Sidekiq.default_worker_options
256
- end
257
- end
258
224
  end
259
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"
@@ -16,8 +16,8 @@ module Sidekiq
16
16
  include Sidekiq::Worker
17
17
 
18
18
  def perform(yml)
19
- (target, method_name, args) = YAML.load(yml)
20
- 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)
21
21
  # The email method can return nil, which causes ActionMailer to return
22
22
  # an undeliverable empty message.
23
23
  if msg
@@ -18,8 +18,8 @@ module Sidekiq
18
18
  include Sidekiq::Worker
19
19
 
20
20
  def perform(yml)
21
- (target, method_name, args) = YAML.load(yml)
22
- 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)
23
23
  end
24
24
  end
25
25
 
@@ -16,8 +16,8 @@ module Sidekiq
16
16
  include Sidekiq::Worker
17
17
 
18
18
  def perform(yml)
19
- (target, method_name, args) = YAML.load(yml)
20
- 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)
21
21
  end
22
22
  end
23
23
 
@@ -13,13 +13,13 @@ 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" }
data/lib/sidekiq/fetch.rb CHANGED
@@ -79,9 +79,10 @@ module Sidekiq
79
79
  if @strictly_ordered_queues
80
80
  @queues
81
81
  else
82
- queues = @queues.shuffle!.uniq
83
- queues << TIMEOUT
84
- queues
82
+ permute = @queues.shuffle
83
+ permute.uniq!
84
+ permute << TIMEOUT
85
+ permute
85
86
  end
86
87
  end
87
88
  end
data/lib/sidekiq/job.rb CHANGED
@@ -1,8 +1,13 @@
1
1
  require "sidekiq/worker"
2
2
 
3
3
  module Sidekiq
4
- # Sidekiq::Job is a new alias for Sidekiq::Worker, coming in 6.3.0.
5
- # You can opt into this by requiring 'sidekiq/job' in your initializer
6
- # and then using `include Sidekiq::Job` rather than `Sidekiq::Worker`.
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.
7
12
  Job = Worker
8
13
  end
@@ -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,11 +53,12 @@ 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
@@ -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
@@ -69,10 +69,12 @@ module Sidekiq
69
69
 
70
70
  private unless $TESTING
71
71
 
72
+ BEAT_PAUSE = 5
73
+
72
74
  def start_heartbeat
73
75
  loop do
74
76
  heartbeat
75
- sleep 5
77
+ sleep BEAT_PAUSE
76
78
  end
77
79
  Sidekiq.logger.info("Heartbeat stopping...")
78
80
  end
@@ -211,6 +213,8 @@ module Sidekiq
211
213
  Your Redis network connection is performing extremely poorly.
212
214
  Last RTT readings were #{RTT_READINGS.buffer.inspect}, ideally these should be < 1000.
213
215
  Ensure Redis is running in the same AZ or datacenter as Sidekiq.
216
+ If these values are close to 100,000, that means your Sidekiq process may be
217
+ CPU overloaded; see https://github.com/mperham/sidekiq/discussions/5039
214
218
  EOM
215
219
  RTT_READINGS.reset
216
220
  end
@@ -55,9 +55,6 @@ module Sidekiq
55
55
  fire_event(:quiet, reverse: true)
56
56
  end
57
57
 
58
- # hack for quicker development / testing environment #2774
59
- PAUSE_TIME = $stdout.tty? ? 0.1 : 0.5
60
-
61
58
  def stop(deadline)
62
59
  quiet
63
60
  fire_event(:shutdown, reverse: true)
@@ -69,12 +66,7 @@ module Sidekiq
69
66
  return if @workers.empty?
70
67
 
71
68
  logger.info { "Pausing to allow workers to finish..." }
72
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
73
- while remaining > PAUSE_TIME
74
- return if @workers.empty?
75
- sleep PAUSE_TIME
76
- remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
77
- end
69
+ wait_for(deadline) { @workers.empty? }
78
70
  return if @workers.empty?
79
71
 
80
72
  hard_shutdown
@@ -130,6 +122,12 @@ module Sidekiq
130
122
  cleanup.each do |processor|
131
123
  processor.kill
132
124
  end
125
+
126
+ # when this method returns, we immediately call `exit` which may not give
127
+ # the remaining threads time to run `ensure` blocks, etc. We pause here up
128
+ # to 3 seconds to give threads a minimal amount of time to run `ensure` blocks.
129
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + 3
130
+ wait_for(deadline) { @workers.empty? }
133
131
  end
134
132
  end
135
133
  end
@@ -0,0 +1,57 @@
1
+ require "active_support/current_attributes"
2
+
3
+ module Sidekiq
4
+ ##
5
+ # Automatically save and load any current attributes in the execution context
6
+ # so context attributes "flow" from Rails actions into any associated jobs.
7
+ # This can be useful for multi-tenancy, i18n locale, timezone, any implicit
8
+ # per-request attribute. See +ActiveSupport::CurrentAttributes+.
9
+ #
10
+ # @example
11
+ #
12
+ # # in your initializer
13
+ # require "sidekiq/middleware/current_attributes"
14
+ # Sidekiq::CurrentAttributes.persist(Myapp::Current)
15
+ #
16
+ module CurrentAttributes
17
+ class Save
18
+ def initialize(cattr)
19
+ @klass = cattr
20
+ end
21
+
22
+ def call(_, job, _, _)
23
+ attrs = @klass.attributes
24
+ if job.has_key?("cattr")
25
+ job["cattr"].merge!(attrs)
26
+ else
27
+ job["cattr"] = attrs
28
+ end
29
+ yield
30
+ end
31
+ end
32
+
33
+ class Load
34
+ def initialize(cattr)
35
+ @klass = cattr
36
+ end
37
+
38
+ def call(_, job, _, &block)
39
+ if job.has_key?("cattr")
40
+ @klass.set(job["cattr"], &block)
41
+ else
42
+ yield
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.persist(klass)
48
+ Sidekiq.configure_client do |config|
49
+ config.client_middleware.add Save, klass
50
+ end
51
+ Sidekiq.configure_server do |config|
52
+ config.client_middleware.add Save, klass
53
+ config.server_middleware.add Load, klass
54
+ end
55
+ end
56
+ end
57
+ end