sidekiq 7.0.9 → 7.3.10

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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +263 -0
  3. data/README.md +5 -5
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiqload +41 -14
  6. data/lib/active_job/queue_adapters/sidekiq_adapter.rb +75 -0
  7. data/lib/generators/sidekiq/job_generator.rb +2 -0
  8. data/lib/sidekiq/api.rb +170 -52
  9. data/lib/sidekiq/capsule.rb +8 -3
  10. data/lib/sidekiq/cli.rb +6 -2
  11. data/lib/sidekiq/client.rb +58 -22
  12. data/lib/sidekiq/component.rb +23 -1
  13. data/lib/sidekiq/config.rb +52 -11
  14. data/lib/sidekiq/deploy.rb +4 -2
  15. data/lib/sidekiq/embedded.rb +2 -0
  16. data/lib/sidekiq/fetch.rb +2 -1
  17. data/lib/sidekiq/iterable_job.rb +55 -0
  18. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  19. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  20. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  21. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  22. data/lib/sidekiq/job/iterable.rb +294 -0
  23. data/lib/sidekiq/job.rb +15 -8
  24. data/lib/sidekiq/job_logger.rb +7 -6
  25. data/lib/sidekiq/job_retry.rb +30 -8
  26. data/lib/sidekiq/job_util.rb +6 -2
  27. data/lib/sidekiq/launcher.rb +8 -6
  28. data/lib/sidekiq/logger.rb +1 -1
  29. data/lib/sidekiq/metrics/query.rb +6 -1
  30. data/lib/sidekiq/metrics/shared.rb +16 -5
  31. data/lib/sidekiq/metrics/tracking.rb +20 -8
  32. data/lib/sidekiq/middleware/current_attributes.rb +88 -16
  33. data/lib/sidekiq/middleware/i18n.rb +2 -0
  34. data/lib/sidekiq/middleware/modules.rb +2 -0
  35. data/lib/sidekiq/monitor.rb +2 -1
  36. data/lib/sidekiq/paginator.rb +8 -2
  37. data/lib/sidekiq/processor.rb +44 -33
  38. data/lib/sidekiq/rails.rb +28 -7
  39. data/lib/sidekiq/redis_client_adapter.rb +30 -31
  40. data/lib/sidekiq/redis_connection.rb +49 -9
  41. data/lib/sidekiq/ring_buffer.rb +3 -0
  42. data/lib/sidekiq/scheduled.rb +3 -3
  43. data/lib/sidekiq/systemd.rb +2 -0
  44. data/lib/sidekiq/testing.rb +32 -13
  45. data/lib/sidekiq/transaction_aware_client.rb +20 -5
  46. data/lib/sidekiq/version.rb +5 -1
  47. data/lib/sidekiq/web/action.rb +29 -7
  48. data/lib/sidekiq/web/application.rb +64 -25
  49. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  50. data/lib/sidekiq/web/helpers.rb +95 -35
  51. data/lib/sidekiq/web/router.rb +5 -2
  52. data/lib/sidekiq/web.rb +67 -3
  53. data/lib/sidekiq.rb +5 -3
  54. data/sidekiq.gemspec +5 -13
  55. data/web/assets/javascripts/application.js +27 -0
  56. data/web/assets/javascripts/dashboard-charts.js +40 -12
  57. data/web/assets/javascripts/dashboard.js +14 -10
  58. data/web/assets/javascripts/metrics.js +34 -0
  59. data/web/assets/stylesheets/application-rtl.css +10 -0
  60. data/web/assets/stylesheets/application.css +38 -3
  61. data/web/locales/en.yml +5 -1
  62. data/web/locales/fr.yml +13 -0
  63. data/web/locales/gd.yml +0 -1
  64. data/web/locales/it.yml +32 -1
  65. data/web/locales/ja.yml +0 -1
  66. data/web/locales/pt-br.yml +20 -1
  67. data/web/locales/tr.yml +100 -0
  68. data/web/locales/uk.yml +24 -1
  69. data/web/locales/zh-cn.yml +0 -1
  70. data/web/locales/zh-tw.yml +0 -1
  71. data/web/views/_footer.erb +12 -1
  72. data/web/views/_job_info.erb +1 -1
  73. data/web/views/_metrics_period_select.erb +1 -1
  74. data/web/views/_summary.erb +7 -7
  75. data/web/views/busy.erb +7 -7
  76. data/web/views/dashboard.erb +29 -36
  77. data/web/views/filtering.erb +6 -0
  78. data/web/views/layout.erb +6 -6
  79. data/web/views/metrics.erb +38 -30
  80. data/web/views/metrics_for_job.erb +30 -39
  81. data/web/views/morgue.erb +2 -2
  82. data/web/views/queue.erb +1 -1
  83. data/web/views/queues.erb +6 -2
  84. metadata +52 -21
@@ -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,15 +100,27 @@ 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]
106
- @totals = Hash.new(0)
107
- @jobs = Hash.new(0)
108
- @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
114
+ reset_instance_variables
109
115
  array
110
116
  }
111
117
  end
118
+
119
+ def reset_instance_variables
120
+ @totals = Hash.new(0)
121
+ @jobs = Hash.new(0)
122
+ @grams = Hash.new { |hash, key| hash[key] = Histogram.new(key) }
123
+ end
112
124
  end
113
125
 
114
126
  class Middleware
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/current_attributes"
2
4
 
3
5
  module Sidekiq
@@ -7,50 +9,120 @@ module Sidekiq
7
9
  # This can be useful for multi-tenancy, i18n locale, timezone, any implicit
8
10
  # per-request attribute. See +ActiveSupport::CurrentAttributes+.
9
11
  #
12
+ # For multiple current attributes, pass an array of current attributes.
13
+ #
10
14
  # @example
11
15
  #
12
16
  # # in your initializer
13
17
  # require "sidekiq/middleware/current_attributes"
14
18
  # Sidekiq::CurrentAttributes.persist("Myapp::Current")
19
+ # # or multiple current attributes
20
+ # Sidekiq::CurrentAttributes.persist(["Myapp::Current", "Myapp::OtherCurrent"])
15
21
  #
16
22
  module CurrentAttributes
17
23
  class Save
18
24
  include Sidekiq::ClientMiddleware
19
25
 
20
- def initialize(cattr)
21
- @strklass = cattr
26
+ def initialize(cattrs)
27
+ @cattrs = cattrs
22
28
  end
23
29
 
24
30
  def call(_, job, _, _)
25
- if !job.has_key?("cattr")
26
- attrs = @strklass.constantize.attributes
27
- # Retries can push the job N times, we don't
28
- # want retries to reset cattr. #5692, #5090
29
- job["cattr"] = attrs if attrs.any?
31
+ @cattrs.each do |(key, strklass)|
32
+ if !job.has_key?(key)
33
+ attrs = strklass.constantize.attributes
34
+ # Retries can push the job N times, we don't
35
+ # want retries to reset cattr. #5692, #5090
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
45
+ end
30
46
  end
31
47
  yield
32
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
33
56
  end
34
57
 
35
58
  class Load
36
59
  include Sidekiq::ServerMiddleware
37
60
 
38
- def initialize(cattr)
39
- @strklass = cattr
61
+ def initialize(cattrs)
62
+ @cattrs = cattrs
40
63
  end
41
64
 
42
65
  def call(_, job, _, &block)
43
- if job.has_key?("cattr")
44
- @strklass.constantize.set(job["cattr"], &block)
45
- else
46
- yield
66
+ klass_attrs = {}
67
+
68
+ @cattrs.each do |(key, strklass)|
69
+ next unless job.has_key?(key)
70
+
71
+ klass_attrs[strklass.constantize] = job[key]
72
+ end
73
+
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
47
97
  end
48
98
  end
49
99
  end
50
100
 
51
- def self.persist(klass, config = Sidekiq.default_configuration)
52
- config.client_middleware.add Save, klass.to_s
53
- config.server_middleware.add Load, klass.to_s
101
+ class << self
102
+ def persist(klass_or_array, config = Sidekiq.default_configuration)
103
+ cattrs = build_cattrs_hash(klass_or_array)
104
+
105
+ config.client_middleware.add Save, cattrs
106
+ config.server_middleware.prepend Load, cattrs
107
+ end
108
+
109
+ private
110
+
111
+ def build_cattrs_hash(klass_or_array)
112
+ if klass_or_array.is_a?(Array)
113
+ {}.tap do |hash|
114
+ klass_or_array.each_with_index do |klass, index|
115
+ hash[key_at(index)] = klass.to_s
116
+ end
117
+ end
118
+ else
119
+ {key_at(0) => klass_or_array.to_s}
120
+ end
121
+ end
122
+
123
+ def key_at(index)
124
+ (index == 0) ? "cattr" : "cattr_#{index}"
125
+ end
54
126
  end
55
127
  end
56
128
  end
@@ -11,6 +11,7 @@ module Sidekiq::Middleware::I18n
11
11
  # to be sent to Sidekiq.
12
12
  class Client
13
13
  include Sidekiq::ClientMiddleware
14
+
14
15
  def call(_jobclass, job, _queue, _redis)
15
16
  job["locale"] ||= I18n.locale
16
17
  yield
@@ -20,6 +21,7 @@ module Sidekiq::Middleware::I18n
20
21
  # Pull the msg locale out and set the current thread to use it.
21
22
  class Server
22
23
  include Sidekiq::ServerMiddleware
24
+
23
25
  def call(_jobclass, job, _queue, &block)
24
26
  I18n.with_locale(job.fetch("locale", I18n.default_locale), &block)
25
27
  end
@@ -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
@@ -19,9 +25,9 @@ module Sidekiq
19
25
  total_size, items = conn.multi { |transaction|
20
26
  transaction.zcard(key)
21
27
  if rev
22
- transaction.zrevrange(key, starting, ending, withscores: true)
28
+ transaction.zrange(key, starting, ending, "REV", "withscores")
23
29
  else
24
- transaction.zrange(key, starting, ending, withscores: true)
30
+ transaction.zrange(key, starting, ending, "withscores")
25
31
  end
26
32
  }
27
33
  [current_page, total_size, items]
@@ -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
@@ -148,6 +153,8 @@ module Sidekiq
148
153
 
149
154
  IGNORE_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :never}
150
155
  private_constant :IGNORE_SHUTDOWN_INTERRUPTS
156
+ ALLOW_SHUTDOWN_INTERRUPTS = {Sidekiq::Shutdown => :immediate}
157
+ private_constant :ALLOW_SHUTDOWN_INTERRUPTS
151
158
 
152
159
  def process(uow)
153
160
  jobstr = uow.job
@@ -171,42 +178,46 @@ module Sidekiq
171
178
  end
172
179
 
173
180
  ack = false
174
- begin
175
- dispatch(job_hash, queue, jobstr) do |inst|
176
- config.server_middleware.invoke(inst, job_hash, queue) do
177
- execute_job(inst, job_hash["args"])
181
+ Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
182
+ Thread.handle_interrupt(ALLOW_SHUTDOWN_INTERRUPTS) do
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"])
186
+ end
178
187
  end
188
+ ack = true
189
+ rescue Sidekiq::Shutdown
190
+ # Had to force kill this job because it didn't finish
191
+ # within the timeout. Don't acknowledge the work since
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
198
+ rescue Sidekiq::JobRetry::Handled => h
199
+ # this is the common case: job raised error and Sidekiq::JobRetry::Handled
200
+ # signals that we created a retry successfully. We can acknowledge the job.
201
+ ack = true
202
+ e = h.cause || h
203
+ handle_exception(e, {context: "Job raised exception", job: job_hash})
204
+ raise e
205
+ rescue Exception => ex
206
+ # Unexpected error! This is very bad and indicates an exception that got past
207
+ # the retry subsystem (e.g. network partition). We won't acknowledge the job
208
+ # so it can be rescued when using Sidekiq Pro.
209
+ handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
210
+ raise ex
179
211
  end
180
- ack = true
181
- rescue Sidekiq::Shutdown
182
- # Had to force kill this job because it didn't finish
183
- # within the timeout. Don't acknowledge the work since
184
- # we didn't properly finish it.
185
- rescue Sidekiq::JobRetry::Handled => h
186
- # this is the common case: job raised error and Sidekiq::JobRetry::Handled
187
- # signals that we created a retry successfully. We can acknowlege the job.
188
- ack = true
189
- e = h.cause || h
190
- handle_exception(e, {context: "Job raised exception", job: job_hash})
191
- raise e
192
- rescue Exception => ex
193
- # Unexpected error! This is very bad and indicates an exception that got past
194
- # the retry subsystem (e.g. network partition). We won't acknowledge the job
195
- # so it can be rescued when using Sidekiq Pro.
196
- handle_exception(ex, {context: "Internal exception!", job: job_hash, jobstr: jobstr})
197
- raise ex
198
212
  ensure
199
213
  if ack
200
- # We don't want a shutdown signal to interrupt job acknowledgment.
201
- Thread.handle_interrupt(IGNORE_SHUTDOWN_INTERRUPTS) do
202
- uow.acknowledge
203
- end
214
+ uow.acknowledge
204
215
  end
205
216
  end
206
217
  end
207
218
 
208
- def execute_job(inst, cloned_args)
209
- inst.perform(*cloned_args)
219
+ def execute_job(instance, cloned_args)
220
+ instance.perform(*cloned_args)
210
221
  end
211
222
 
212
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)
@@ -20,6 +31,10 @@ module Sidekiq
20
31
  def inspect
21
32
  "#<Sidekiq::Rails::Reloader @app=#{@app.class.name}>"
22
33
  end
34
+
35
+ def to_hash
36
+ {app: @app.class.name}
37
+ end
23
38
  end
24
39
 
25
40
  # By including the Options module, we allow AJs to directly control sidekiq features
@@ -35,18 +50,14 @@ module Sidekiq
35
50
  # end
36
51
  initializer "sidekiq.active_job_integration" do
37
52
  ActiveSupport.on_load(:active_job) do
53
+ require_relative "../active_job/queue_adapters/sidekiq_adapter"
38
54
  include ::Sidekiq::Job::Options unless respond_to?(:sidekiq_options)
39
55
  end
40
56
  end
41
57
 
42
- initializer "sidekiq.rails_logger" do
58
+ initializer "sidekiq.backtrace_cleaner" do
43
59
  Sidekiq.configure_server do |config|
44
- # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
45
- # it will appear in the Sidekiq console with all of the job context. See #5021 and
46
- # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
47
- unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
48
- ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
49
- end
60
+ config[:backtrace_cleaner] = ->(backtrace) { ::Rails.backtrace_cleaner.clean(backtrace) }
50
61
  end
51
62
  end
52
63
 
@@ -57,6 +68,16 @@ module Sidekiq
57
68
  config.after_initialize do
58
69
  Sidekiq.configure_server do |config|
59
70
  config[:reloader] = Sidekiq::Rails::Reloader.new
71
+
72
+ # This is the integration code necessary so that if a job uses `Rails.logger.info "Hello"`,
73
+ # it will appear in the Sidekiq console with all of the job context.
74
+ unless ::Rails.logger == config.logger || ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
75
+ if ::Rails.logger.respond_to?(:broadcast_to)
76
+ ::Rails.logger.broadcast_to(config.logger)
77
+ else
78
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(config.logger))
79
+ end
80
+ end
60
81
  end
61
82
  end
62
83
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require "redis_client"
4
5
  require "redis_client/decorator"
5
6
 
@@ -8,22 +9,40 @@ module Sidekiq
8
9
  BaseError = RedisClient::Error
9
10
  CommandError = RedisClient::CommandError
10
11
 
12
+ # You can add/remove items or clear the whole thing if you don't want deprecation warnings.
13
+ DEPRECATED_COMMANDS = %i[rpoplpush zrangebyscore zrevrange zrevrangebyscore getset hmset setex setnx].to_set
14
+
11
15
  module CompatMethods
12
- # TODO Deprecate and remove this
13
16
  def info
14
17
  @client.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
15
18
  end
16
19
 
17
- # TODO Deprecate and remove this
18
20
  def evalsha(sha, keys, argv)
19
21
  @client.call("EVALSHA", sha, keys.size, *keys, *argv)
20
22
  end
21
23
 
24
+ # this is the set of Redis commands used by Sidekiq. Not guaranteed
25
+ # to be comprehensive, we use this as a performance enhancement to
26
+ # avoid calling method_missing on most commands
27
+ USED_COMMANDS = %w[bitfield bitfield_ro del exists expire flushdb
28
+ get hdel hget hgetall hincrby hlen hmget hset hsetnx incr incrby
29
+ lindex llen lmove lpop lpush lrange lrem mget mset ping pttl
30
+ publish rpop rpush sadd scard script set sismember smembers
31
+ srem ttl type unlink zadd zcard zincrby zrange zrem
32
+ zremrangebyrank zremrangebyscore]
33
+
34
+ USED_COMMANDS.each do |name|
35
+ define_method(name) do |*args, **kwargs|
36
+ @client.call(name, *args, **kwargs)
37
+ end
38
+ end
39
+
22
40
  private
23
41
 
24
42
  # this allows us to use methods like `conn.hmset(...)` instead of having to use
25
43
  # redis-client's native `conn.call("hmset", ...)`
26
44
  def method_missing(*args, &block)
45
+ warn("[sidekiq#5788] Redis has deprecated the `#{args.first}`command, called at #{caller(1..1)}") if DEPRECATED_COMMANDS.include?(args.first)
27
46
  @client.call(*args, *block)
28
47
  end
29
48
  ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
@@ -39,34 +58,19 @@ module Sidekiq
39
58
  def config
40
59
  @client.config
41
60
  end
42
-
43
- def message
44
- yield nil, @queue.pop
45
- end
46
-
47
- # NB: this method does not return
48
- def subscribe(chan)
49
- @queue = ::Queue.new
50
-
51
- pubsub = @client.pubsub
52
- pubsub.call("subscribe", chan)
53
-
54
- loop do
55
- evt = pubsub.next_event
56
- next if evt.nil?
57
- next unless evt[0] == "message" && evt[1] == chan
58
-
59
- (_, _, msg) = evt
60
- @queue << msg
61
- yield self
62
- end
63
- end
64
61
  end
65
62
 
66
63
  def initialize(options)
67
64
  opts = client_opts(options)
68
65
  @config = if opts.key?(:sentinels)
69
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)
70
74
  else
71
75
  RedisClient.config(**opts)
72
76
  end
@@ -82,8 +86,7 @@ module Sidekiq
82
86
  opts = options.dup
83
87
 
84
88
  if opts[:namespace]
85
- raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature isn't supported by redis-client. " \
86
- "Either use the redis adapter or remove the namespace."
89
+ raise ArgumentError, "Your Redis configuration uses the namespace '#{opts[:namespace]}' but this feature is no longer supported in Sidekiq 7+. See https://github.com/sidekiq/sidekiq/blob/main/docs/7.0-Upgrade.md#redis-namespace."
87
90
  end
88
91
 
89
92
  opts.delete(:size)
@@ -94,13 +97,9 @@ module Sidekiq
94
97
  opts.delete(:network_timeout)
95
98
  end
96
99
 
97
- if opts[:driver]
98
- opts[:driver] = opts[:driver].to_sym
99
- end
100
-
101
100
  opts[:name] = opts.delete(:master_name) if opts.key?(:master_name)
102
101
  opts[:role] = opts[:role].to_sym if opts.key?(:role)
103
- opts.delete(:url) if opts.key?(:sentinels)
102
+ opts[:driver] = opts[:driver].to_sym if opts.key?(:driver)
104
103
 
105
104
  # Issue #3303, redis-rb will silently retry an operation.
106
105
  # This can lead to duplicate jobs if Sidekiq::Client's LPUSH
@@ -8,16 +8,30 @@ 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
+ symbolized_options[:password] = wrap(symbolized_options[:password]) if symbolized_options.key?(:password)
14
+ symbolized_options[:sentinel_password] = wrap(symbolized_options[:sentinel_password]) if symbolized_options.key?(:sentinel_password)
13
15
 
14
16
  logger = symbolized_options.delete(:logger)
15
17
  logger&.info { "Sidekiq #{Sidekiq::VERSION} connecting to Redis with options #{scrub(symbolized_options)}" }
16
18
 
19
+ raise "Sidekiq 7+ does not support Redis protocol 2" if symbolized_options[:protocol] == 2
20
+
21
+ safe = !!symbolized_options.delete(:cluster_safe)
22
+ raise ":nodes not allowed, Sidekiq is not safe to run on Redis Cluster" if !safe && symbolized_options.key?(:nodes)
23
+
17
24
  size = symbolized_options.delete(:size) || 5
18
25
  pool_timeout = symbolized_options.delete(:pool_timeout) || 1
19
26
  pool_name = symbolized_options.delete(:pool_name)
20
27
 
28
+ # Default timeout in redis-client is 1 second, which can be too aggressive
29
+ # if the Sidekiq process is CPU-bound. With 10-15 threads and a thread quantum of 100ms,
30
+ # it can be easy to get the occasional ReadTimeoutError. You can still provide
31
+ # a smaller timeout explicitly:
32
+ # config.redis = { url: "...", timeout: 1 }
33
+ symbolized_options[:timeout] ||= 3
34
+
21
35
  redis_config = Sidekiq::RedisClientAdapter.new(symbolized_options)
22
36
  ConnectionPool.new(timeout: pool_timeout, size: size, name: pool_name) do
23
37
  redis_config.new_client
@@ -26,23 +40,51 @@ module Sidekiq
26
40
 
27
41
  private
28
42
 
43
+ # Wrap hard-coded passwords in a Proc to avoid logging the value
44
+ def wrap(pwd)
45
+ if pwd.is_a?(String)
46
+ ->(username) { pwd }
47
+ else
48
+ pwd
49
+ end
50
+ end
51
+
52
+ def deep_symbolize_keys(object)
53
+ case object
54
+ when Hash
55
+ object.each_with_object({}) do |(key, value), result|
56
+ result[key.to_sym] = deep_symbolize_keys(value)
57
+ end
58
+ when Array
59
+ object.map { |e| deep_symbolize_keys(e) }
60
+ else
61
+ object
62
+ end
63
+ end
64
+
29
65
  def scrub(options)
30
66
  redacted = "REDACTED"
31
67
 
32
68
  # Deep clone so we can muck with these options all we want and exclude
33
69
  # params from dump-and-load that may contain objects that Marshal is
34
70
  # unable to safely dump.
35
- keys = options.keys - [:logger, :ssl_params]
71
+ keys = options.keys - [:logger, :ssl_params, :password, :sentinel_password]
36
72
  scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
37
73
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
38
74
  uri.password = redacted
39
75
  scrubbed_options[:url] = uri.to_s
40
76
  end
41
- if scrubbed_options[:password]
42
- scrubbed_options[:password] = redacted
43
- end
77
+ scrubbed_options[:password] = redacted if options.key?(:password)
78
+ scrubbed_options[:sentinel_password] = redacted if options.key?(:sentinel_password)
44
79
  scrubbed_options[:sentinels]&.each do |sentinel|
45
- sentinel[:password] = redacted if sentinel[:password]
80
+ if sentinel.is_a?(String)
81
+ if (uri = URI(sentinel)) && uri.password
82
+ uri.password = redacted
83
+ sentinel.replace(uri.to_s)
84
+ end
85
+ elsif sentinel[:password]
86
+ sentinel[:password] = redacted
87
+ end
46
88
  end
47
89
  scrubbed_options
48
90
  end
@@ -66,9 +108,7 @@ module Sidekiq
66
108
  EOM
67
109
  end
68
110
 
69
- ENV[
70
- p || "REDIS_URL"
71
- ]
111
+ ENV[p.to_s] || ENV["REDIS_URL"]
72
112
  end
73
113
  end
74
114
  end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "forwardable"
2
4
 
3
5
  module Sidekiq
4
6
  class RingBuffer
5
7
  include Enumerable
6
8
  extend Forwardable
9
+
7
10
  def_delegators :@buf, :[], :each, :size
8
11
 
9
12
  def initialize(size, default = 0)