sidekiq 6.2.0 → 6.3.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +63 -1
  3. data/LICENSE +1 -1
  4. data/README.md +2 -2
  5. data/lib/sidekiq/api.rb +80 -56
  6. data/lib/sidekiq/cli.rb +10 -2
  7. data/lib/sidekiq/client.rb +2 -6
  8. data/lib/sidekiq/extensions/generic_proxy.rb +3 -1
  9. data/lib/sidekiq/fetch.rb +5 -4
  10. data/lib/sidekiq/job.rb +13 -0
  11. data/lib/sidekiq/job_logger.rb +1 -1
  12. data/lib/sidekiq/job_retry.rb +3 -7
  13. data/lib/sidekiq/launcher.rb +27 -21
  14. data/lib/sidekiq/middleware/chain.rb +5 -3
  15. data/lib/sidekiq/middleware/current_attributes.rb +52 -0
  16. data/lib/sidekiq/rails.rb +11 -0
  17. data/lib/sidekiq/redis_connection.rb +4 -6
  18. data/lib/sidekiq/scheduled.rb +40 -15
  19. data/lib/sidekiq/testing.rb +1 -3
  20. data/lib/sidekiq/util.rb +28 -0
  21. data/lib/sidekiq/version.rb +1 -1
  22. data/lib/sidekiq/web/action.rb +2 -2
  23. data/lib/sidekiq/web/application.rb +7 -5
  24. data/lib/sidekiq/web/csrf_protection.rb +9 -6
  25. data/lib/sidekiq/web/helpers.rb +9 -21
  26. data/lib/sidekiq/web.rb +7 -6
  27. data/lib/sidekiq/worker.rb +72 -5
  28. data/lib/sidekiq.rb +3 -1
  29. data/sidekiq.gemspec +1 -1
  30. data/web/assets/javascripts/application.js +82 -61
  31. data/web/assets/javascripts/dashboard.js +51 -51
  32. data/web/assets/stylesheets/application-dark.css +18 -31
  33. data/web/assets/stylesheets/application-rtl.css +0 -4
  34. data/web/assets/stylesheets/application.css +21 -233
  35. data/web/locales/ar.yml +8 -2
  36. data/web/locales/en.yml +4 -1
  37. data/web/locales/es.yml +18 -2
  38. data/web/locales/fr.yml +7 -0
  39. data/web/locales/ja.yml +3 -0
  40. data/web/locales/lt.yml +1 -1
  41. data/web/views/_footer.erb +1 -1
  42. data/web/views/_job_info.erb +1 -1
  43. data/web/views/_poll_link.erb +2 -5
  44. data/web/views/_summary.erb +7 -7
  45. data/web/views/busy.erb +8 -7
  46. data/web/views/dashboard.erb +22 -14
  47. data/web/views/dead.erb +1 -1
  48. data/web/views/layout.erb +1 -1
  49. data/web/views/morgue.erb +6 -6
  50. data/web/views/queue.erb +11 -11
  51. data/web/views/queues.erb +3 -3
  52. data/web/views/retries.erb +7 -7
  53. data/web/views/retry.erb +1 -1
  54. data/web/views/scheduled.erb +1 -1
  55. metadata +5 -3
@@ -0,0 +1,52 @@
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
+ job["cattr"] = @klass.attributes
24
+ yield
25
+ end
26
+ end
27
+
28
+ class Load
29
+ def initialize(cattr)
30
+ @klass = cattr
31
+ end
32
+
33
+ def call(_, job, _, &block)
34
+ if job.has_key?("cattr")
35
+ @klass.set(job["cattr"], &block)
36
+ else
37
+ yield
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.persist(klass)
43
+ Sidekiq.configure_client do |config|
44
+ config.client_middleware.add Save, klass
45
+ end
46
+ Sidekiq.configure_server do |config|
47
+ config.client_middleware.add Save, klass
48
+ config.server_middleware.add Load, klass
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/sidekiq/rails.rb CHANGED
@@ -37,6 +37,17 @@ module Sidekiq
37
37
  end
38
38
  end
39
39
 
40
+ initializer "sidekiq.rails_logger" do
41
+ Sidekiq.configure_server do |_|
42
+ # This is the integration code necessary so that if code uses `Rails.logger.info "Hello"`,
43
+ # it will appear in the Sidekiq console with all of the job context. See #5021 and
44
+ # https://github.com/rails/rails/blob/b5f2b550f69a99336482739000c58e4e04e033aa/railties/lib/rails/commands/server/server_command.rb#L82-L84
45
+ unless ::ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
46
+ ::Rails.logger.extend(::ActiveSupport::Logger.broadcast(::Sidekiq.logger))
47
+ end
48
+ end
49
+ end
50
+
40
51
  # This hook happens after all initializers are run, just before returning
41
52
  # from config/environment.rb back to sidekiq/cli.rb.
42
53
  #
@@ -94,12 +94,10 @@ module Sidekiq
94
94
  def log_info(options)
95
95
  redacted = "REDACTED"
96
96
 
97
- # deep clone so we can muck with these options all we want
98
- #
99
- # exclude SSL params from dump-and-load because some information isn't
100
- # safely dumpable in current Rubies
101
- keys = options.keys
102
- keys.delete(:ssl_params)
97
+ # Deep clone so we can muck with these options all we want and exclude
98
+ # params from dump-and-load that may contain objects that Marshal is
99
+ # unable to safely dump.
100
+ keys = options.keys - [:logger, :ssl_params]
103
101
  scrubbed_options = Marshal.load(Marshal.dump(options.slice(*keys)))
104
102
  if scrubbed_options[:url] && (uri = URI.parse(scrubbed_options[:url])) && uri.password
105
103
  uri.password = redacted
@@ -9,29 +9,48 @@ module Sidekiq
9
9
  SETS = %w[retry schedule]
10
10
 
11
11
  class Enq
12
+ LUA_ZPOPBYSCORE = <<~LUA
13
+ local key, now = KEYS[1], ARGV[1]
14
+ local jobs = redis.call("zrangebyscore", key, "-inf", now, "limit", 0, 1)
15
+ if jobs[1] then
16
+ redis.call("zrem", key, jobs[1])
17
+ return jobs[1]
18
+ end
19
+ LUA
20
+
21
+ def initialize
22
+ @lua_zpopbyscore_sha = nil
23
+ end
24
+
12
25
  def enqueue_jobs(now = Time.now.to_f.to_s, sorted_sets = SETS)
13
26
  # A job's "score" in Redis is the time at which it should be processed.
14
27
  # Just check Redis for the set of jobs with a timestamp before now.
15
28
  Sidekiq.redis do |conn|
16
29
  sorted_sets.each do |sorted_set|
17
- # Get next items in the queue with scores (time to execute) <= now.
18
- until (jobs = conn.zrangebyscore(sorted_set, "-inf", now, limit: [0, 100])).empty?
19
- # We need to go through the list one at a time to reduce the risk of something
20
- # going wrong between the time jobs are popped from the scheduled queue and when
21
- # they are pushed onto a work queue and losing the jobs.
22
- jobs.each do |job|
23
- # Pop item off the queue and add it to the work queue. If the job can't be popped from
24
- # the queue, it's because another process already popped it so we can move on to the
25
- # next one.
26
- if conn.zrem(sorted_set, job)
27
- Sidekiq::Client.push(Sidekiq.load_json(job))
28
- Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
29
- end
30
- end
30
+ # Get next item in the queue with score (time to execute) <= now.
31
+ # We need to go through the list one at a time to reduce the risk of something
32
+ # going wrong between the time jobs are popped from the scheduled queue and when
33
+ # they are pushed onto a work queue and losing the jobs.
34
+ while (job = zpopbyscore(conn, keys: [sorted_set], argv: [now]))
35
+ Sidekiq::Client.push(Sidekiq.load_json(job))
36
+ Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" }
31
37
  end
32
38
  end
33
39
  end
34
40
  end
41
+
42
+ private
43
+
44
+ def zpopbyscore(conn, keys: nil, argv: nil)
45
+ @lua_zpopbyscore_sha = conn.script(:load, LUA_ZPOPBYSCORE) if @lua_zpopbyscore_sha.nil?
46
+
47
+ conn.evalsha(@lua_zpopbyscore_sha, keys: keys, argv: argv)
48
+ rescue Redis::CommandError => e
49
+ raise unless e.message.start_with?("NOSCRIPT")
50
+
51
+ @lua_zpopbyscore_sha = nil
52
+ retry
53
+ end
35
54
  end
36
55
 
37
56
  ##
@@ -49,6 +68,7 @@ module Sidekiq
49
68
  @sleeper = ConnectionPool::TimedStack.new
50
69
  @done = false
51
70
  @thread = nil
71
+ @count_calls = 0
52
72
  end
53
73
 
54
74
  # Shut down this instance, will pause until the thread is dead.
@@ -152,8 +172,13 @@ module Sidekiq
152
172
  end
153
173
 
154
174
  def process_count
155
- pcount = Sidekiq::ProcessSet.new.size
175
+ # The work buried within Sidekiq::ProcessSet#cleanup can be
176
+ # expensive at scale. Cut it down by 90% with this counter.
177
+ # NB: This method is only called by the scheduler thread so we
178
+ # don't need to worry about the thread safety of +=.
179
+ pcount = Sidekiq::ProcessSet.new(@count_calls % 10 == 0).size
156
180
  pcount = 1 if pcount == 0
181
+ @count_calls += 1
157
182
  pcount
158
183
  end
159
184
 
@@ -338,7 +338,5 @@ module Sidekiq
338
338
  end
339
339
 
340
340
  if defined?(::Rails) && Rails.respond_to?(:env) && !Rails.env.test? && !$TESTING
341
- puts("**************************************************")
342
- puts("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.")
343
- puts("**************************************************")
341
+ warn("⛔️ WARNING: Sidekiq testing API enabled, but this is not the test environment. Your jobs will not go to Redis.", uplevel: 1)
344
342
  end
data/lib/sidekiq/util.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
3
4
  require "socket"
4
5
  require "securerandom"
5
6
  require "sidekiq/exception_handler"
@@ -8,6 +9,33 @@ module Sidekiq
8
9
  ##
9
10
  # This module is part of Sidekiq core and not intended for extensions.
10
11
  #
12
+
13
+ class RingBuffer
14
+ include Enumerable
15
+ extend Forwardable
16
+ def_delegators :@buf, :[], :each, :size
17
+
18
+ def initialize(size, default = 0)
19
+ @size = size
20
+ @buf = Array.new(size, default)
21
+ @index = 0
22
+ end
23
+
24
+ def <<(element)
25
+ @buf[@index % @size] = element
26
+ @index += 1
27
+ element
28
+ end
29
+
30
+ def buffer
31
+ @buf
32
+ end
33
+
34
+ def reset(default = 0)
35
+ @buf.fill(default)
36
+ end
37
+ end
38
+
11
39
  module Util
12
40
  include ExceptionHandler
13
41
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidekiq
4
- VERSION = "6.2.0"
4
+ VERSION = "6.3.1"
5
5
  end
@@ -15,7 +15,7 @@ module Sidekiq
15
15
  end
16
16
 
17
17
  def halt(res)
18
- throw :halt, res
18
+ throw :halt, [res, {"Content-Type" => "text/plain"}, [res.to_s]]
19
19
  end
20
20
 
21
21
  def redirect(location)
@@ -68,7 +68,7 @@ module Sidekiq
68
68
  end
69
69
 
70
70
  def json(payload)
71
- [200, {"Content-Type" => "application/json", "Cache-Control" => "no-cache"}, [Sidekiq.dump_json(payload)]]
71
+ [200, {"Content-Type" => "application/json", "Cache-Control" => "private, no-store"}, [Sidekiq.dump_json(payload)]]
72
72
  end
73
73
 
74
74
  def initialize(env, block)
@@ -82,15 +82,17 @@ module Sidekiq
82
82
  erb(:queues)
83
83
  end
84
84
 
85
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
86
+
85
87
  get "/queues/:name" do
86
88
  @name = route_params[:name]
87
89
 
88
- halt(404) unless @name
90
+ halt(404) if !@name || @name !~ QUEUE_NAME
89
91
 
90
92
  @count = (params["count"] || 25).to_i
91
93
  @queue = Sidekiq::Queue.new(@name)
92
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
93
- @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
94
+ (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
95
+ @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
94
96
 
95
97
  erb(:queue)
96
98
  end
@@ -111,7 +113,7 @@ module Sidekiq
111
113
 
112
114
  post "/queues/:name/delete" do
113
115
  name = route_params[:name]
114
- Sidekiq::Job.new(params["key_val"], name).delete
116
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
115
117
 
116
118
  redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
117
119
  end
@@ -312,7 +314,7 @@ module Sidekiq
312
314
  # rendered content goes here
313
315
  headers = {
314
316
  "Content-Type" => "text/html",
315
- "Cache-Control" => "no-cache",
317
+ "Cache-Control" => "private, no-store",
316
318
  "Content-Language" => action.locale,
317
319
  "Content-Security-Policy" => CSP_HEADER
318
320
  }
@@ -77,16 +77,19 @@ module Sidekiq
77
77
  end
78
78
 
79
79
 
80
- If this is a bare Rack app, use a session middleware before Sidekiq::Web:
80
+ If this is a Rails app in API mode, you need to enable sessions.
81
+
82
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
81
83
 
84
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
82
85
 
83
- # first, use IRB to create a shared secret key for sessions and commit it
84
- require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
86
+ # first, use IRB to create a shared secret key for sessions and commit it
87
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
85
88
 
89
+ # now use the secret with a session cookie middleware
90
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
91
+ run Sidekiq::Web
86
92
 
87
- # now use the secret with a session cookie middleware
88
- use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
89
- run Sidekiq::Web
90
93
  EOM
91
94
  end
92
95
 
@@ -10,14 +10,13 @@ module Sidekiq
10
10
  module WebHelpers
11
11
  def strings(lang)
12
12
  @strings ||= {}
13
- @strings[lang] ||= begin
14
- # Allow sidekiq-web extensions to add locale paths
15
- # so extensions can be localized
16
- settings.locales.each_with_object({}) do |path, global|
17
- find_locale_files(lang).each do |file|
18
- strs = YAML.load(File.open(file))
19
- global.merge!(strs[lang])
20
- end
13
+
14
+ # Allow sidekiq-web extensions to add locale paths
15
+ # so extensions can be localized
16
+ @strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
17
+ find_locale_files(lang).each do |file|
18
+ strs = YAML.load(File.open(file))
19
+ global.merge!(strs[lang])
21
20
  end
22
21
  end
23
22
  end
@@ -71,17 +70,6 @@ module Sidekiq
71
70
  @head_html.join if defined?(@head_html)
72
71
  end
73
72
 
74
- def poll_path
75
- if current_path != "" && params["poll"]
76
- path = root_path + current_path
77
- query_string = to_query_string(params.slice(*params.keys - %w[page poll]))
78
- path += "?#{query_string}" unless query_string.empty?
79
- path
80
- else
81
- ""
82
- end
83
- end
84
-
85
73
  def text_direction
86
74
  get_locale["TextDirection"] || "ltr"
87
75
  end
@@ -126,7 +114,7 @@ module Sidekiq
126
114
  # within is used by Sidekiq Pro
127
115
  def display_tags(job, within = nil)
128
116
  job.tags.map { |tag|
129
- "<span class='jobtag label label-info'>#{::Rack::Utils.escape_html(tag)}</span>"
117
+ "<span class='label label-info jobtag'>#{::Rack::Utils.escape_html(tag)}</span>"
130
118
  }.join(" ")
131
119
  end
132
120
 
@@ -204,7 +192,7 @@ module Sidekiq
204
192
  [score.to_f, jid]
205
193
  end
206
194
 
207
- SAFE_QPARAMS = %w[page poll direction]
195
+ SAFE_QPARAMS = %w[page direction]
208
196
 
209
197
  # Merge options with current params, filter safe params, and stringify to query string
210
198
  def qparams(options)
data/lib/sidekiq/web.rb CHANGED
@@ -143,13 +143,14 @@ module Sidekiq
143
143
  klass = self.class
144
144
  m = middlewares
145
145
 
146
+ rules = []
147
+ rules = [[:all, {"Cache-Control" => "public, max-age=86400"}]] unless ENV["SIDEKIQ_WEB_TESTING"]
148
+
146
149
  ::Rack::Builder.new do
147
- use Rack::Static, :urls => ["/stylesheets", "/images", "/javascripts"],
148
- :root => ASSETS,
149
- :cascade => true,
150
- :header_rules => [
151
- [:all, {'Cache-Control' => 'public, max-age=86400'}],
152
- ]
150
+ use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
151
+ root: ASSETS,
152
+ cascade: true,
153
+ header_rules: rules
153
154
  m.each { |middleware, block| use(*middleware, &block) }
154
155
  use Sidekiq::Web::CsrfProtection unless $TESTING
155
156
  run WebApplication.new(klass)
@@ -9,6 +9,7 @@ module Sidekiq
9
9
  #
10
10
  # class HardWorker
11
11
  # include Sidekiq::Worker
12
+ # sidekiq_options queue: 'critical', retry: 5
12
13
  #
13
14
  # def perform(*args)
14
15
  # # do some work
@@ -20,6 +21,26 @@ module Sidekiq
20
21
  # HardWorker.perform_async(1, 2, 3)
21
22
  #
22
23
  # Note that perform_async is a class method, perform is an instance method.
24
+ #
25
+ # Sidekiq::Worker also includes several APIs to provide compatibility with
26
+ # ActiveJob.
27
+ #
28
+ # class SomeWorker
29
+ # include Sidekiq::Worker
30
+ # queue_as :critical
31
+ #
32
+ # def perform(...)
33
+ # end
34
+ # end
35
+ #
36
+ # SomeWorker.set(wait_until: 1.hour).perform_async(123)
37
+ #
38
+ # Note that arguments passed to the job must still obey Sidekiq's
39
+ # best practice for simple, JSON-native data types. Sidekiq will not
40
+ # implement ActiveJob's more complex argument serialization. For
41
+ # this reason, we don't implement `perform_later` as our call semantics
42
+ # are very different.
43
+ #
23
44
  module Worker
24
45
  ##
25
46
  # The Options module is extracted so we can include it in ActiveJob::Base
@@ -153,10 +174,16 @@ module Sidekiq
153
174
  def initialize(klass, opts)
154
175
  @klass = klass
155
176
  @opts = opts
177
+
178
+ # ActiveJob compatibility
179
+ interval = @opts.delete(:wait_until) || @opts.delete(:wait)
180
+ at(interval) if interval
156
181
  end
157
182
 
158
183
  def set(options)
184
+ interval = options.delete(:wait_until) || options.delete(:wait)
159
185
  @opts.merge!(options)
186
+ at(interval) if interval
160
187
  self
161
188
  end
162
189
 
@@ -164,19 +191,29 @@ module Sidekiq
164
191
  @klass.client_push(@opts.merge("args" => args, "class" => @klass))
165
192
  end
166
193
 
194
+ def perform_bulk(args, batch_size: 1_000)
195
+ args.each_slice(batch_size).flat_map do |slice|
196
+ Sidekiq::Client.push_bulk(@opts.merge("class" => @klass, "args" => slice))
197
+ end
198
+ end
199
+
167
200
  # +interval+ must be a timestamp, numeric or something that acts
168
201
  # numeric (like an activesupport time interval).
169
202
  def perform_in(interval, *args)
203
+ at(interval).perform_async(*args)
204
+ end
205
+ alias_method :perform_at, :perform_in
206
+
207
+ private
208
+
209
+ def at(interval)
170
210
  int = interval.to_f
171
211
  now = Time.now.to_f
172
212
  ts = (int < 1_000_000_000 ? now + int : int)
173
-
174
- payload = @opts.merge("class" => @klass, "args" => args)
175
213
  # Optimization to enqueue something now that is scheduled to go out now or in the past
176
- payload["at"] = ts if ts > now
177
- @klass.client_push(payload)
214
+ @opts["at"] = ts if ts > now
215
+ self
178
216
  end
179
- alias_method :perform_at, :perform_in
180
217
  end
181
218
 
182
219
  module ClassMethods
@@ -192,6 +229,10 @@ module Sidekiq
192
229
  raise ArgumentError, "Do not call .delay_until on a Sidekiq::Worker class, call .perform_at"
193
230
  end
194
231
 
232
+ def queue_as(q)
233
+ sidekiq_options("queue" => q.to_s)
234
+ end
235
+
195
236
  def set(options)
196
237
  Setter.new(self, options)
197
238
  end
@@ -200,6 +241,32 @@ module Sidekiq
200
241
  client_push("class" => self, "args" => args)
201
242
  end
202
243
 
244
+ ##
245
+ # Push a large number of jobs to Redis, while limiting the batch of
246
+ # each job payload to 1,000. This method helps cut down on the number
247
+ # of round trips to Redis, which can increase the performance of enqueueing
248
+ # large numbers of jobs.
249
+ #
250
+ # +items+ must be an Array of Arrays.
251
+ #
252
+ # For finer-grained control, use `Sidekiq::Client.push_bulk` directly.
253
+ #
254
+ # Example (3 Redis round trips):
255
+ #
256
+ # SomeWorker.perform_async(1)
257
+ # SomeWorker.perform_async(2)
258
+ # SomeWorker.perform_async(3)
259
+ #
260
+ # Would instead become (1 Redis round trip):
261
+ #
262
+ # SomeWorker.perform_bulk([[1], [2], [3]])
263
+ #
264
+ def perform_bulk(items, batch_size: 1_000)
265
+ items.each_slice(batch_size).flat_map do |slice|
266
+ Sidekiq::Client.push_bulk("class" => self, "args" => slice)
267
+ end
268
+ end
269
+
203
270
  # +interval+ must be a timestamp, numeric or something that acts
204
271
  # numeric (like an activesupport time interval).
205
272
  def perform_in(interval, *args)
data/lib/sidekiq.rb CHANGED
@@ -6,6 +6,7 @@ fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.5.0." i
6
6
  require "sidekiq/logger"
7
7
  require "sidekiq/client"
8
8
  require "sidekiq/worker"
9
+ require "sidekiq/job"
9
10
  require "sidekiq/redis_connection"
10
11
  require "sidekiq/delay"
11
12
 
@@ -100,7 +101,8 @@ module Sidekiq
100
101
  # 2550 Failover can cause the server to become a replica, need
101
102
  # to disconnect and reopen the socket to get back to the primary.
102
103
  # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
103
- if retryable && ex.message =~ /READONLY|NOREPLICAS/
104
+ # 4985 Use the same logic when a blocking command is force-unblocked
105
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
104
106
  conn.disconnect!
105
107
  retryable = false
106
108
  retry
data/sidekiq.gemspec CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
18
18
  "homepage_uri" => "https://sidekiq.org",
19
19
  "bug_tracker_uri" => "https://github.com/mperham/sidekiq/issues",
20
20
  "documentation_uri" => "https://github.com/mperham/sidekiq/wiki",
21
- "changelog_uri" => "https://github.com/mperham/sidekiq/blob/master/Changes.md",
21
+ "changelog_uri" => "https://github.com/mperham/sidekiq/blob/main/Changes.md",
22
22
  "source_code_uri" => "https://github.com/mperham/sidekiq"
23
23
  }
24
24