sidekiq 7.2.4 → 7.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +116 -0
  3. data/README.md +1 -1
  4. data/bin/sidekiqload +21 -12
  5. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  6. data/lib/generators/sidekiq/job_generator.rb +2 -0
  7. data/lib/sidekiq/api.rb +63 -34
  8. data/lib/sidekiq/capsule.rb +8 -3
  9. data/lib/sidekiq/cli.rb +2 -1
  10. data/lib/sidekiq/client.rb +21 -1
  11. data/lib/sidekiq/component.rb +22 -0
  12. data/lib/sidekiq/config.rb +27 -3
  13. data/lib/sidekiq/deploy.rb +2 -0
  14. data/lib/sidekiq/embedded.rb +2 -0
  15. data/lib/sidekiq/fetch.rb +1 -1
  16. data/lib/sidekiq/iterable_job.rb +55 -0
  17. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  18. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  19. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  20. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  21. data/lib/sidekiq/job/iterable.rb +294 -0
  22. data/lib/sidekiq/job.rb +13 -2
  23. data/lib/sidekiq/job_logger.rb +7 -6
  24. data/lib/sidekiq/job_retry.rb +6 -1
  25. data/lib/sidekiq/job_util.rb +2 -0
  26. data/lib/sidekiq/launcher.rb +1 -1
  27. data/lib/sidekiq/metrics/query.rb +2 -0
  28. data/lib/sidekiq/metrics/shared.rb +15 -4
  29. data/lib/sidekiq/metrics/tracking.rb +13 -5
  30. data/lib/sidekiq/middleware/current_attributes.rb +46 -13
  31. data/lib/sidekiq/middleware/modules.rb +2 -0
  32. data/lib/sidekiq/monitor.rb +2 -1
  33. data/lib/sidekiq/paginator.rb +6 -0
  34. data/lib/sidekiq/processor.rb +20 -10
  35. data/lib/sidekiq/rails.rb +12 -0
  36. data/lib/sidekiq/redis_client_adapter.rb +8 -5
  37. data/lib/sidekiq/redis_connection.rb +33 -2
  38. data/lib/sidekiq/ring_buffer.rb +2 -0
  39. data/lib/sidekiq/systemd.rb +2 -0
  40. data/lib/sidekiq/testing.rb +5 -5
  41. data/lib/sidekiq/version.rb +5 -1
  42. data/lib/sidekiq/web/action.rb +21 -4
  43. data/lib/sidekiq/web/application.rb +43 -82
  44. data/lib/sidekiq/web/helpers.rb +62 -15
  45. data/lib/sidekiq/web/router.rb +5 -2
  46. data/lib/sidekiq/web.rb +54 -2
  47. data/lib/sidekiq.rb +5 -3
  48. data/sidekiq.gemspec +3 -2
  49. data/web/assets/javascripts/application.js +6 -1
  50. data/web/assets/javascripts/dashboard-charts.js +24 -12
  51. data/web/assets/javascripts/dashboard.js +7 -1
  52. data/web/assets/stylesheets/application.css +16 -3
  53. data/web/locales/en.yml +3 -1
  54. data/web/locales/fr.yml +0 -1
  55. data/web/locales/gd.yml +0 -1
  56. data/web/locales/it.yml +32 -1
  57. data/web/locales/ja.yml +0 -1
  58. data/web/locales/pt-br.yml +1 -2
  59. data/web/locales/tr.yml +100 -0
  60. data/web/locales/uk.yml +24 -1
  61. data/web/locales/zh-cn.yml +0 -1
  62. data/web/locales/zh-tw.yml +0 -1
  63. data/web/views/_footer.erb +1 -2
  64. data/web/views/dashboard.erb +10 -7
  65. data/web/views/filtering.erb +1 -2
  66. data/web/views/layout.erb +6 -6
  67. data/web/views/metrics.erb +7 -8
  68. data/web/views/metrics_for_job.erb +4 -4
  69. data/web/views/morgue.erb +2 -2
  70. data/web/views/queue.erb +1 -1
  71. metadata +32 -13
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
  require "time"
3
5
 
@@ -36,8 +36,8 @@ module Sidekiq
36
36
  # has a heartbeat thread, caller can use `async_beat: false`
37
37
  # and instead have thread call Launcher#heartbeat every N seconds.
38
38
  def run(async_beat: true)
39
- Sidekiq.freeze!
40
39
  logger.debug { @config.merge!({}) }
40
+ Sidekiq.freeze!
41
41
  @thread = safe_thread("heartbeat", &method(:start_heartbeat)) if async_beat
42
42
  @poller.start
43
43
  @managers.each(&:start)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sidekiq"
2
4
  require "date"
3
5
  require "set"
@@ -1,10 +1,21 @@
1
- require "concurrent"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
4
  module Metrics
5
- # This is the only dependency on concurrent-ruby in Sidekiq but it's
6
- # mandatory for thread-safety until MRI supports atomic operations on values.
7
- Counter = ::Concurrent::AtomicFixnum
5
+ class Counter
6
+ def initialize
7
+ @value = 0
8
+ @lock = Mutex.new
9
+ end
10
+
11
+ def increment
12
+ @lock.synchronize { @value += 1 }
13
+ end
14
+
15
+ def value
16
+ @lock.synchronize { @value }
17
+ end
18
+ end
8
19
 
9
20
  # Implements space-efficient but statistically useful histogram storage.
10
21
  # A precise time histogram stores every time. Instead we break times into a set of
@@ -31,11 +31,11 @@ module Sidekiq
31
31
  # We don't track time for failed jobs as they can have very unpredictable
32
32
  # execution times. more important to know average time for successful jobs so we
33
33
  # can better recognize when a perf regression is introduced.
34
- @lock.synchronize {
35
- @grams[klass].record_time(time_ms)
36
- @jobs["#{klass}|ms"] += time_ms
37
- @totals["ms"] += time_ms
38
- }
34
+ track_time(klass, time_ms)
35
+ rescue JobRetry::Skip
36
+ # This is raised when iterable job is interrupted.
37
+ track_time(klass, time_ms)
38
+ raise
39
39
  rescue Exception
40
40
  @lock.synchronize {
41
41
  @jobs["#{klass}|f"] += 1
@@ -100,6 +100,14 @@ module Sidekiq
100
100
 
101
101
  private
102
102
 
103
+ def track_time(klass, time_ms)
104
+ @lock.synchronize {
105
+ @grams[klass].record_time(time_ms)
106
+ @jobs["#{klass}|ms"] += time_ms
107
+ @totals["ms"] += time_ms
108
+ }
109
+ end
110
+
103
111
  def reset
104
112
  @lock.synchronize {
105
113
  array = [@totals, @jobs, @grams]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/current_attributes"
2
4
 
3
5
  module Sidekiq
@@ -31,11 +33,26 @@ module Sidekiq
31
33
  attrs = strklass.constantize.attributes
32
34
  # Retries can push the job N times, we don't
33
35
  # want retries to reset cattr. #5692, #5090
34
- job[key] = attrs if attrs.any?
36
+ if attrs.any?
37
+ # Older rails has a bug that `CurrentAttributes#attributes` always returns
38
+ # the same hash instance. We need to dup it to avoid being accidentally mutated.
39
+ job[key] = if returns_same_object?
40
+ attrs.dup
41
+ else
42
+ attrs
43
+ end
44
+ end
35
45
  end
36
46
  end
37
47
  yield
38
48
  end
49
+
50
+ private
51
+
52
+ def returns_same_object?
53
+ ActiveSupport::VERSION::MAJOR < 8 ||
54
+ (ActiveSupport::VERSION::MAJOR == 8 && ActiveSupport::VERSION::MINOR == 0)
55
+ end
39
56
  end
40
57
 
41
58
  class Load
@@ -46,22 +63,38 @@ module Sidekiq
46
63
  end
47
64
 
48
65
  def call(_, job, _, &block)
49
- cattrs_to_reset = []
66
+ klass_attrs = {}
50
67
 
51
68
  @cattrs.each do |(key, strklass)|
52
- if job.has_key?(key)
53
- constklass = strklass.constantize
54
- cattrs_to_reset << constklass
69
+ next unless job.has_key?(key)
55
70
 
56
- job[key].each do |(attribute, value)|
57
- constklass.public_send(:"#{attribute}=", value)
58
- end
59
- end
71
+ klass_attrs[strklass.constantize] = job[key]
60
72
  end
61
73
 
62
- yield
63
- ensure
64
- cattrs_to_reset.each(&:reset)
74
+ wrap(klass_attrs.to_a, &block)
75
+ end
76
+
77
+ private
78
+
79
+ def wrap(klass_attrs, &block)
80
+ klass, attrs = klass_attrs.shift
81
+ return block.call unless klass
82
+
83
+ retried = false
84
+
85
+ begin
86
+ klass.set(attrs) do
87
+ wrap(klass_attrs, &block)
88
+ end
89
+ rescue NoMethodError
90
+ raise if retried
91
+
92
+ # It is possible that the `CurrentAttributes` definition
93
+ # was changed before the job started processing.
94
+ attrs = attrs.select { |attr| klass.respond_to?(attr) }
95
+ retried = true
96
+ retry
97
+ end
65
98
  end
66
99
  end
67
100
 
@@ -70,7 +103,7 @@ module Sidekiq
70
103
  cattrs = build_cattrs_hash(klass_or_array)
71
104
 
72
105
  config.client_middleware.add Save, cattrs
73
- config.server_middleware.add Load, cattrs
106
+ config.server_middleware.prepend Load, cattrs
74
107
  end
75
108
 
76
109
  private
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  # Server-side middleware must import this Module in order
3
5
  # to get access to server resources during `call`.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "fileutils"
4
5
  require "sidekiq/api"
@@ -98,7 +99,7 @@ class Sidekiq::Monitor
98
99
  pad = opts[:pad] || 0
99
100
  max_length = opts[:max_length] || (80 - pad)
100
101
  out = []
101
- line = ""
102
+ line = +""
102
103
  values.each do |value|
103
104
  if (line.length + value.length) > max_length
104
105
  out << line
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Paginator
5
+ TYPE_CACHE = {
6
+ "dead" => "zset",
7
+ "retry" => "zset",
8
+ "schedule" => "zset"
9
+ }
10
+
5
11
  def page(key, pageidx = 1, page_size = 25, opts = nil)
6
12
  current_page = (pageidx.to_i < 1) ? 1 : pageidx.to_i
7
13
  pageidx = current_page - 1
@@ -36,7 +36,7 @@ module Sidekiq
36
36
  @job = nil
37
37
  @thread = nil
38
38
  @reloader = Sidekiq.default_configuration[:reloader]
39
- @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(logger)
39
+ @job_logger = (capsule.config[:job_logger] || Sidekiq::JobLogger).new(capsule.config)
40
40
  @retrier = Sidekiq::JobRetry.new(capsule)
41
41
  end
42
42
 
@@ -58,6 +58,10 @@ module Sidekiq
58
58
  @thread.value if wait
59
59
  end
60
60
 
61
+ def stopping?
62
+ @done
63
+ end
64
+
61
65
  def start
62
66
  @thread ||= safe_thread("#{config.name}/processor", &method(:run))
63
67
  end
@@ -134,10 +138,11 @@ module Sidekiq
134
138
  # Effectively this block denotes a "unit of work" to Rails.
135
139
  @reloader.call do
136
140
  klass = Object.const_get(job_hash["class"])
137
- inst = klass.new
138
- inst.jid = job_hash["jid"]
139
- @retrier.local(inst, jobstr, queue) do
140
- yield inst
141
+ instance = klass.new
142
+ instance.jid = job_hash["jid"]
143
+ instance._context = self
144
+ @retrier.local(instance, jobstr, queue) do
145
+ yield instance
141
146
  end
142
147
  end
143
148
  end
@@ -175,9 +180,9 @@ module Sidekiq
175
180
  ack = false
176
181
  Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
177
182
  Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
178
- dispatch(job_hash, queue, jobstr) do |inst|
179
- config.server_middleware.invoke(inst, job_hash, queue) do
180
- execute_job(inst, job_hash["args"])
183
+ dispatch(job_hash, queue, jobstr) do |instance|
184
+ config.server_middleware.invoke(instance, job_hash, queue) do
185
+ execute_job(instance, job_hash["args"])
181
186
  end
182
187
  end
183
188
  ack = true
@@ -185,6 +190,11 @@ module Sidekiq
185
190
  # Had to force kill this job because it didn't finish
186
191
  # within the timeout. Don't acknowledge the work since
187
192
  # we didn't properly finish it.
193
+ rescue Sidekiq::JobRetry::Skip => s
194
+ # Skip means we handled this error elsewhere. We don't
195
+ # need to log or report the error.
196
+ ack = true
197
+ raise s
188
198
  rescue Sidekiq::JobRetry::Handled => h
189
199
  # this is the common case: job raised error and Sidekiq::JobRetry::Handled
190
200
  # signals that we created a retry successfully. We can acknowledge the job.
@@ -206,8 +216,8 @@ module Sidekiq
206
216
  end
207
217
  end
208
218
 
209
- def execute_job(inst, cloned_args)
210
- inst.perform(*cloned_args)
219
+ def execute_job(instance, cloned_args)
220
+ instance.perform(*cloned_args)
211
221
  end
212
222
 
213
223
  # Ruby doesn't provide atomic counters out of the box so we'll
data/lib/sidekiq/rails.rb CHANGED
@@ -4,6 +4,17 @@ require "sidekiq/job"
4
4
  require "rails"
5
5
 
6
6
  module Sidekiq
7
+ module ActiveJob
8
+ # @api private
9
+ class Wrapper
10
+ include Sidekiq::Job
11
+
12
+ def perform(job_data)
13
+ ::ActiveJob::Base.execute(job_data.merge("provider_job_id" => jid))
14
+ end
15
+ end
16
+ end
17
+
7
18
  class Rails < ::Rails::Engine
8
19
  class Reloader
9
20
  def initialize(app = ::Rails.application)
@@ -39,6 +50,7 @@ module Sidekiq
39
50
  # end
40
51
  initializer "sidekiq.active_job_integration" do
41
52
  ActiveSupport.on_load(:active_job) do
53
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
42
54
  include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
43
55
  end
44
56
  end
@@ -64,6 +64,13 @@ module Sidekiq
64
64
  opts = client_opts(options)
65
65
  @config = if opts.key?(:sentinels)
66
66
  RedisClient.sentinel(**opts)
67
+ elsif opts.key?(:nodes)
68
+ # Sidekiq does not support Redis clustering but Sidekiq Enterprise's
69
+ # rate limiters are cluster-safe so we can scale to millions
70
+ # of rate limiters using a Redis cluster. This requires the
71
+ # `redis-cluster-client` gem.
72
+ # Sidekiq::Limiter.redis = { nodes: [...] }
73
+ RedisClient.cluster(**opts)
67
74
  else
68
75
  RedisClient.config(**opts)
69
76
  end
@@ -90,13 +97,9 @@ module Sidekiq
90
97
  opts.delete(:network_timeout)
91
98
  end
92
99
 
93
- if opts[:driver]
94
- opts[:driver] = opts[:driver].to_sym
95
- end
96
-
97
100
  opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
98
101
  opts[:role] = opts[:role].to_sym if opts.key?(:role)
99
- opts.delete(:url) if opts.key?(:sentinels)
102
+ opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
100
103
 
101
104
  # Issue #3303, redis-rb will silently retry an operation.
102
105
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -8,17 +8,28 @@ module Sidekiq
8
8
  module RedisConnection
9
9
  class << self
10
10
  def create(options = {})
11
- symbolized_options = options.transform_keys(&:to_sym)
11
+ symbolized_options = deep_symbolize_keys(options)
12
12
  symbolized_options[:url] ||= determine_redis_provider
13
13
 
14
14
  logger = symbolized_options.delete(:logger)
15
15
  logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
16
16
 
17
17
  raise "Sidekiq 7+ does not support Redis protocol 2" if symbolized_options[:protocol] == 2
18
+
19
+ safe = !!symbolized_options.delete(:cluster_safe)
20
+ raise ":nodes not allowed, Sidekiq is not safe to run on Redis Cluster" if !safe && symbolized_options.key?(:nodes)
21
+
18
22
  size = symbolized_options.delete(:size) || 5
19
23
  pool_timeout = symbolized_options.delete(:pool_timeout) || 1
20
24
  pool_name = symbolized_options.delete(:pool_name)
21
25
 
26
+ # Default timeout in redis-client is 1 second, which can be too aggressive
27
+ # if the Sidekiq process is CPU-bound. With 10-15 threads and a thread quantum of 100ms,
28
+ # it can be easy to get the occasional ReadTimeoutError. You can still provide
29
+ # a smaller timeout explicitly:
30
+ # config.redis = { url: "...", timeout: 1 }
31
+ symbolized_options[:timeout] ||= 3
32
+
22
33
  redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
23
34
  ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
24
35
  redis_config.new_client
@@ -27,6 +38,19 @@ module Sidekiq
27
38
 
28
39
  private
29
40
 
41
+ def deep_symbolize_keys(object)
42
+ case object
43
+ when Hash
44
+ object.each_with_object({}) do |(key, value), result|
45
+ result[key.to_sym] = deep_symbolize_keys(value)
46
+ end
47
+ when Array
48
+ object.map { |e| deep_symbolize_keys(e) }
49
+ else
50
+ object
51
+ end
52
+ end
53
+
30
54
  def scrub(options)
31
55
  redacted = "REDACTED"
32
56
 
@@ -42,7 +66,14 @@ module Sidekiq
42
66
  scrubbed_options[:password] = redacted if scrubbed_options[:password]
43
67
  scrubbed_options[:sentinel_password] = redacted if scrubbed_options[:sentinel_password]
44
68
  scrubbed_options[:sentinels]&.each do |sentinel|
45
- sentinel[:password] = redacted if sentinel[:password]
69
+ if sentinel.is_a?(String)
70
+ if (uri = URI(sentinel)) && uri.password
71
+ uri.password = redacted
72
+ sentinel.replace(uri.to_s)
73
+ end
74
+ elsif sentinel[:password]
75
+ sentinel[:password] = redacted
76
+ end
46
77
  end
47
78
  scrubbed_options
48
79
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  module Sidekiq
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # Sidekiq's systemd integration allows Sidekiq to inform systemd:
3
5
  # 1. when it has successfully started
@@ -283,11 +283,11 @@ module Sidekiq
283
283
  end
284
284
 
285
285
  def process_job(job)
286
- inst = new
287
- inst.jid = job["jid"]
288
- inst.bid = job["bid"] if inst.respond_to?(:bid=)
289
- Sidekiq::Testing.server_middleware.invoke(inst, job, job["queue"]) do
290
- execute_job(inst, job["args"])
286
+ instance = new
287
+ instance.jid = job["jid"]
288
+ instance.bid = job["bid"] if instance.respond_to?(:bid=)
289
+ Sidekiq::Testing.server_middleware.invoke(instance, job, job["queue"]) do
290
+ execute_job(instance, job["args"])
291
291
  end
292
292
  end
293
293
 
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "7.2.4"
4
+ VERSION = "7.3.9"
5
5
  MAJOR = 7
6
+
7
+ def self.gem_version
8
+ Gem::Version.new(VERSION)
9
+ end
6
10
  end
@@ -27,6 +27,7 @@ module Sidekiq
27
27
  redirect current_location
28
28
  end
29
29
 
30
+ # deprecated, will warn in 8.0
30
31
  def params
31
32
  indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
32
33
 
@@ -36,8 +37,19 @@ module Sidekiq
36
37
  indifferent_hash
37
38
  end
38
39
 
39
- def route_params
40
- env[WebRouter::ROUTE_PARAMS]
40
+ # Use like `url_params("page")` within your action blocks
41
+ def url_params(key)
42
+ request.params[key]
43
+ end
44
+
45
+ # Use like `route_params(:name)` within your action blocks
46
+ # key is required in 8.0, nil is only used for backwards compatibility
47
+ def route_params(key = nil)
48
+ if key
49
+ env[WebRouter::ROUTE_PARAMS][key]
50
+ else
51
+ env[WebRouter::ROUTE_PARAMS]
52
+ end
41
53
  end
42
54
 
43
55
  def session
@@ -47,8 +59,13 @@ module Sidekiq
47
59
  def erb(content, options = {})
48
60
  if content.is_a? Symbol
49
61
  unless respond_to?(:"_erb_#{content}")
50
- src = ERB.new(File.read("#{Web.settings.views}/#{content}.erb")).src
51
- WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
62
+ views = options[:views] || Web.settings.views
63
+ filename = "#{views}/#{content}.erb"
64
+ src = ERB.new(File.read(filename)).src
65
+
66
+ # Need to use lineno less by 1 because erb generates a
67
+ # comment before the source code.
68
+ WebAction.class_eval <<-RUBY, filename, -1 # standard:disable Style/EvalWithLocation
52
69
  def _erb_#{content}
53
70
  #{src}
54
71
  end