sidekiq 6.5.12 → 7.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +224 -20
  3. data/README.md +43 -35
  4. data/bin/multi_queue_bench +271 -0
  5. data/bin/sidekiq +3 -8
  6. data/bin/sidekiqload +204 -118
  7. data/bin/sidekiqmon +3 -0
  8. data/lib/sidekiq/api.rb +187 -135
  9. data/lib/sidekiq/capsule.rb +127 -0
  10. data/lib/sidekiq/cli.rb +59 -75
  11. data/lib/sidekiq/client.rb +66 -37
  12. data/lib/sidekiq/component.rb +4 -1
  13. data/lib/sidekiq/config.rb +287 -0
  14. data/lib/sidekiq/deploy.rb +62 -0
  15. data/lib/sidekiq/embedded.rb +61 -0
  16. data/lib/sidekiq/fetch.rb +11 -14
  17. data/lib/sidekiq/job.rb +371 -10
  18. data/lib/sidekiq/job_logger.rb +2 -2
  19. data/lib/sidekiq/job_retry.rb +36 -18
  20. data/lib/sidekiq/job_util.rb +51 -15
  21. data/lib/sidekiq/launcher.rb +71 -65
  22. data/lib/sidekiq/logger.rb +2 -27
  23. data/lib/sidekiq/manager.rb +9 -11
  24. data/lib/sidekiq/metrics/query.rb +7 -4
  25. data/lib/sidekiq/metrics/shared.rb +8 -7
  26. data/lib/sidekiq/metrics/tracking.rb +27 -21
  27. data/lib/sidekiq/middleware/chain.rb +19 -18
  28. data/lib/sidekiq/middleware/current_attributes.rb +52 -20
  29. data/lib/sidekiq/monitor.rb +16 -3
  30. data/lib/sidekiq/paginator.rb +2 -2
  31. data/lib/sidekiq/processor.rb +46 -51
  32. data/lib/sidekiq/rails.rb +15 -10
  33. data/lib/sidekiq/redis_client_adapter.rb +23 -66
  34. data/lib/sidekiq/redis_connection.rb +15 -117
  35. data/lib/sidekiq/scheduled.rb +22 -23
  36. data/lib/sidekiq/testing.rb +32 -41
  37. data/lib/sidekiq/transaction_aware_client.rb +11 -5
  38. data/lib/sidekiq/version.rb +2 -1
  39. data/lib/sidekiq/web/action.rb +8 -3
  40. data/lib/sidekiq/web/application.rb +108 -15
  41. data/lib/sidekiq/web/csrf_protection.rb +10 -7
  42. data/lib/sidekiq/web/helpers.rb +52 -38
  43. data/lib/sidekiq/web.rb +17 -16
  44. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  45. data/lib/sidekiq.rb +76 -274
  46. data/sidekiq.gemspec +12 -10
  47. data/web/assets/javascripts/application.js +39 -0
  48. data/web/assets/javascripts/base-charts.js +106 -0
  49. data/web/assets/javascripts/dashboard-charts.js +182 -0
  50. data/web/assets/javascripts/dashboard.js +10 -232
  51. data/web/assets/javascripts/metrics.js +151 -115
  52. data/web/assets/stylesheets/application-dark.css +4 -0
  53. data/web/assets/stylesheets/application-rtl.css +10 -89
  54. data/web/assets/stylesheets/application.css +45 -298
  55. data/web/locales/ar.yml +70 -70
  56. data/web/locales/cs.yml +62 -62
  57. data/web/locales/da.yml +60 -53
  58. data/web/locales/de.yml +65 -65
  59. data/web/locales/el.yml +2 -7
  60. data/web/locales/en.yml +78 -70
  61. data/web/locales/es.yml +68 -68
  62. data/web/locales/fa.yml +65 -65
  63. data/web/locales/fr.yml +81 -67
  64. data/web/locales/gd.yml +99 -0
  65. data/web/locales/he.yml +65 -64
  66. data/web/locales/hi.yml +59 -59
  67. data/web/locales/it.yml +53 -53
  68. data/web/locales/ja.yml +67 -69
  69. data/web/locales/ko.yml +52 -52
  70. data/web/locales/lt.yml +66 -66
  71. data/web/locales/nb.yml +61 -61
  72. data/web/locales/nl.yml +52 -52
  73. data/web/locales/pl.yml +45 -45
  74. data/web/locales/pt-br.yml +79 -69
  75. data/web/locales/pt.yml +51 -51
  76. data/web/locales/ru.yml +67 -66
  77. data/web/locales/sv.yml +53 -53
  78. data/web/locales/ta.yml +60 -60
  79. data/web/locales/uk.yml +62 -61
  80. data/web/locales/ur.yml +64 -64
  81. data/web/locales/vi.yml +67 -67
  82. data/web/locales/zh-cn.yml +20 -18
  83. data/web/locales/zh-tw.yml +10 -1
  84. data/web/views/_footer.erb +17 -2
  85. data/web/views/_job_info.erb +18 -2
  86. data/web/views/_metrics_period_select.erb +12 -0
  87. data/web/views/_paging.erb +2 -0
  88. data/web/views/_poll_link.erb +1 -1
  89. data/web/views/_summary.erb +7 -7
  90. data/web/views/busy.erb +46 -35
  91. data/web/views/dashboard.erb +26 -5
  92. data/web/views/filtering.erb +7 -0
  93. data/web/views/metrics.erb +46 -24
  94. data/web/views/metrics_for_job.erb +41 -69
  95. data/web/views/morgue.erb +5 -9
  96. data/web/views/queue.erb +10 -14
  97. data/web/views/queues.erb +9 -3
  98. data/web/views/retries.erb +5 -9
  99. data/web/views/scheduled.erb +12 -13
  100. metadata +44 -38
  101. data/lib/sidekiq/delay.rb +0 -43
  102. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  103. data/lib/sidekiq/extensions/active_record.rb +0 -43
  104. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  105. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  106. data/lib/sidekiq/metrics/deploy.rb +0 -47
  107. data/lib/sidekiq/worker.rb +0 -370
  108. data/web/assets/javascripts/graph.js +0 -16
  109. /data/{LICENSE → LICENSE.txt} +0 -0
@@ -0,0 +1,127 @@
1
+ require "sidekiq/component"
2
+
3
+ module Sidekiq
4
+ # A Sidekiq::Capsule is the set of resources necessary to
5
+ # process one or more queues with a given concurrency.
6
+ # One "default" Capsule is started but the user may declare additional
7
+ # Capsules in their initializer.
8
+ #
9
+ # This capsule will pull jobs from the "single" queue and process
10
+ # the jobs with one thread, meaning the jobs will be processed serially.
11
+ #
12
+ # Sidekiq.configure_server do |config|
13
+ # config.capsule("single-threaded") do |cap|
14
+ # cap.concurrency = 1
15
+ # cap.queues = %w(single)
16
+ # end
17
+ # end
18
+ class Capsule
19
+ include Sidekiq::Component
20
+
21
+ attr_reader :name
22
+ attr_reader :queues
23
+ attr_accessor :concurrency
24
+ attr_reader :mode
25
+ attr_reader :weights
26
+
27
+ def initialize(name, config)
28
+ @name = name
29
+ @config = config
30
+ @queues = ["default"]
31
+ @weights = {"default" => 0}
32
+ @concurrency = config[:concurrency]
33
+ @mode = :strict
34
+ end
35
+
36
+ def fetcher
37
+ @fetcher ||= begin
38
+ inst = (config[:fetch_class] || Sidekiq::BasicFetch).new(self)
39
+ inst.setup(config[:fetch_setup]) if inst.respond_to?(:setup)
40
+ inst
41
+ end
42
+ end
43
+
44
+ def stop
45
+ fetcher&.bulk_requeue([])
46
+ end
47
+
48
+ # Sidekiq checks queues in three modes:
49
+ # - :strict - all queues have 0 weight and are checked strictly in order
50
+ # - :weighted - queues have arbitrary weight between 1 and N
51
+ # - :random - all queues have weight of 1
52
+ def queues=(val)
53
+ @weights = {}
54
+ @queues = Array(val).each_with_object([]) do |qstr, memo|
55
+ arr = qstr
56
+ arr = qstr.split(",") if qstr.is_a?(String)
57
+ name, weight = arr
58
+ @weights[name] = weight.to_i
59
+ [weight.to_i, 1].max.times do
60
+ memo << name
61
+ end
62
+ end
63
+ @mode = if @weights.values.all?(&:zero?)
64
+ :strict
65
+ elsif @weights.values.all? { |x| x == 1 }
66
+ :random
67
+ else
68
+ :weighted
69
+ end
70
+ end
71
+
72
+ # Allow the middleware to be different per-capsule.
73
+ # Avoid if possible and add middleware globally so all
74
+ # capsules share the same chains. Easier to debug that way.
75
+ def client_middleware
76
+ @client_chain ||= config.client_middleware.copy_for(self)
77
+ yield @client_chain if block_given?
78
+ @client_chain
79
+ end
80
+
81
+ def server_middleware
82
+ @server_chain ||= config.server_middleware.copy_for(self)
83
+ yield @server_chain if block_given?
84
+ @server_chain
85
+ end
86
+
87
+ def redis_pool
88
+ Thread.current[:sidekiq_redis_pool] || local_redis_pool
89
+ end
90
+
91
+ def local_redis_pool
92
+ # connection pool is lazy, it will not create connections unless you actually need them
93
+ # so don't be skimpy!
94
+ @redis ||= config.new_redis_pool(@concurrency, name)
95
+ end
96
+
97
+ def redis
98
+ raise ArgumentError, "requires a block" unless block_given?
99
+ redis_pool.with do |conn|
100
+ retryable = true
101
+ begin
102
+ yield conn
103
+ rescue RedisClientAdapter::BaseError => ex
104
+ # 2550 Failover can cause the server to become a replica, need
105
+ # to disconnect and reopen the socket to get back to the primary.
106
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
107
+ # 4985 Use the same logic when a blocking command is force-unblocked
108
+ # The same retry logic is also used in client.rb
109
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
110
+ conn.close
111
+ retryable = false
112
+ retry
113
+ end
114
+ raise
115
+ end
116
+ end
117
+ end
118
+
119
+ def lookup(name)
120
+ config.lookup(name)
121
+ end
122
+
123
+ def logger
124
+ config.logger
125
+ end
126
+ end
127
+ end
data/lib/sidekiq/cli.rb CHANGED
@@ -9,20 +9,11 @@ require "erb"
9
9
  require "fileutils"
10
10
 
11
11
  require "sidekiq"
12
+ require "sidekiq/config"
12
13
  require "sidekiq/component"
14
+ require "sidekiq/capsule"
13
15
  require "sidekiq/launcher"
14
16
 
15
- # module ScoutApm
16
- # VERSION = "5.3.1"
17
- # end
18
- fail <<~EOM if defined?(ScoutApm::VERSION) && ScoutApm::VERSION < "5.2.0"
19
-
20
-
21
- scout_apm v#{ScoutApm::VERSION} is unsafe with Sidekiq 6.5. Please run `bundle up scout_apm` to upgrade to 5.2.0 or greater.
22
-
23
-
24
- EOM
25
-
26
17
  module Sidekiq # :nodoc:
27
18
  class CLI
28
19
  include Sidekiq::Component
@@ -33,9 +24,7 @@ module Sidekiq # :nodoc:
33
24
  attr_accessor :config
34
25
 
35
26
  def parse(args = ARGV.dup)
36
- @config = Sidekiq
37
- @config[:error_handlers].clear
38
- @config[:error_handlers] << @config.method(:default_error_handler)
27
+ @config ||= Sidekiq.default_configuration
39
28
 
40
29
  setup_options(args)
41
30
  initialize_logger
@@ -49,10 +38,10 @@ module Sidekiq # :nodoc:
49
38
  # Code within this method is not tested because it alters
50
39
  # global process state irreversibly. PRs which improve the
51
40
  # test coverage of Sidekiq::CLI are welcomed.
52
- def run(boot_app: true)
41
+ def run(boot_app: true, warmup: true)
53
42
  boot_application if boot_app
54
43
 
55
- if environment == "development" && $stdout.tty? && @config.log_formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
44
+ if environment == "development" && $stdout.tty? && @config.logger.formatter.is_a?(Sidekiq::Logger::Formatters::Pretty)
56
45
  print_banner
57
46
  end
58
47
  logger.info "Booted Rails #{::Rails.version} application in #{environment} environment" if rails_app?
@@ -84,26 +73,27 @@ module Sidekiq # :nodoc:
84
73
  # touch the connection pool so it is created before we
85
74
  # fire startup and start multithreading.
86
75
  info = @config.redis_info
87
- ver = info["redis_version"]
88
- raise "You are connecting to Redis v#{ver}, Sidekiq requires Redis v4.0.0 or greater" if ver < "4"
76
+ ver = Gem::Version.new(info["redis_version"])
77
+ raise "You are connecting to Redis #{ver}, Sidekiq requires Redis 6.2.0 or greater" if ver < Gem::Version.new("6.2.0")
89
78
 
90
79
  maxmemory_policy = info["maxmemory_policy"]
91
- if maxmemory_policy != "noeviction"
80
+ if maxmemory_policy != "noeviction" && maxmemory_policy != ""
81
+ # Redis Enterprise Cloud returns "" for their policy 😳
92
82
  logger.warn <<~EOM
93
83
 
94
84
 
95
85
  WARNING: Your Redis instance will evict Sidekiq data under heavy load.
96
86
  The 'noeviction' maxmemory policy is recommended (current policy: '#{maxmemory_policy}').
97
- See: https://github.com/mperham/sidekiq/wiki/Using-Redis#memory
87
+ See: https://github.com/sidekiq/sidekiq/wiki/Using-Redis#memory
98
88
 
99
89
  EOM
100
90
  end
101
91
 
102
92
  # Since the user can pass us a connection pool explicitly in the initializer, we
103
93
  # need to verify the size is large enough or else Sidekiq's performance is dramatically slowed.
104
- cursize = @config.redis_pool.size
105
- needed = @config[:concurrency] + 2
106
- raise "Your pool of #{cursize} Redis connections is too small, please increase the size to at least #{needed}" if cursize < needed
94
+ @config.capsules.each_pair do |name, cap|
95
+ raise ArgumentError, "Pool size too small for #{name}" if cap.redis_pool.size < cap.concurrency
96
+ end
107
97
 
108
98
  # cache process identity
109
99
  @config[:identity] = identity
@@ -111,12 +101,14 @@ module Sidekiq # :nodoc:
111
101
  # Touch middleware so it isn't lazy loaded by multiple threads, #3043
112
102
  @config.server_middleware
113
103
 
104
+ ::Process.warmup if warmup && ::Process.respond_to?(:warmup)
105
+
114
106
  # Before this point, the process is initializing with just the main thread.
115
107
  # Starting here the process will now have multiple threads running.
116
108
  fire_event(:startup, reverse: false, reraise: true)
117
109
 
118
- logger.debug { "Client Middleware: #{@config.client_middleware.map(&:klass).join(", ")}" }
119
- logger.debug { "Server Middleware: #{@config.server_middleware.map(&:klass).join(", ")}" }
110
+ logger.debug { "Client Middleware: #{@config.default_capsule.client_middleware.map(&:klass).join(", ")}" }
111
+ logger.debug { "Server Middleware: #{@config.default_capsule.server_middleware.map(&:klass).join(", ")}" }
120
112
 
121
113
  launch(self_read)
122
114
  end
@@ -149,19 +141,34 @@ module Sidekiq # :nodoc:
149
141
  end
150
142
  end
151
143
 
152
- def self.w
153
- "\e[37m"
144
+ HOLIDAY_COLORS = {
145
+ # got other color-specific holidays from around the world?
146
+ # https://developer-book.com/post/definitive-guide-for-colored-text-in-terminal/#256-color-escape-codes
147
+ "3-17" => "\e[1;32m", # St. Patrick's Day green
148
+ "10-31" => "\e[38;5;208m" # Halloween orange
149
+ }
150
+
151
+ def self.day
152
+ @@day ||= begin
153
+ t = Date.today
154
+ "#{t.month}-#{t.day}"
155
+ end
154
156
  end
155
157
 
156
158
  def self.r
157
- "\e[31m"
159
+ @@r ||= HOLIDAY_COLORS[day] || "\e[1;31m"
158
160
  end
159
161
 
160
162
  def self.b
161
- "\e[30m"
163
+ @@b ||= HOLIDAY_COLORS[day] || "\e[30m"
164
+ end
165
+
166
+ def self.w
167
+ "\e[1;37m"
162
168
  end
163
169
 
164
170
  def self.reset
171
+ @@b = @@r = @@day = nil
165
172
  "\e[0m"
166
173
  end
167
174
 
@@ -174,7 +181,7 @@ module Sidekiq # :nodoc:
174
181
  #{w} ,$$$$$b#{b}/#{w}md$$$P^'
175
182
  #{w} .d$$$$$$#{b}/#{w}$$$P'
176
183
  #{w} $$^' `"#{b}/#{w}$$$' #{r}____ _ _ _ _
177
- #{w} $: ,$$: #{r} / ___|(_) __| | ___| | _(_) __ _
184
+ #{w} $: #{b}'#{w},$$: #{r} / ___|(_) __| | ___| | _(_) __ _
178
185
  #{w} `b :$$ #{r} \\___ \\| |/ _` |/ _ \\ |/ / |/ _` |
179
186
  #{w} $$: #{r} ___) | | (_| | __/ <| | (_| |
180
187
  #{w} $$ #{r}|____/|_|\\__,_|\\___|_|\\_\\_|\\__, |
@@ -272,6 +279,18 @@ module Sidekiq # :nodoc:
272
279
 
273
280
  # merge with defaults
274
281
  @config.merge!(opts)
282
+
283
+ @config.default_capsule.tap do |cap|
284
+ cap.queues = opts[:queues]
285
+ cap.concurrency = opts[:concurrency] || @config[:concurrency]
286
+ end
287
+
288
+ opts[:capsules]&.each do |name, cap_config|
289
+ @config.capsule(name.to_s) do |cap|
290
+ cap.queues = cap_config[:queues]
291
+ cap.concurrency = cap_config[:concurrency]
292
+ end
293
+ end
275
294
  end
276
295
 
277
296
  def boot_application
@@ -279,12 +298,11 @@ module Sidekiq # :nodoc:
279
298
 
280
299
  if File.directory?(@config[:require])
281
300
  require "rails"
282
- if ::Rails::VERSION::MAJOR < 5
283
- raise "Sidekiq no longer supports this version of Rails"
284
- else
285
- require "sidekiq/rails"
286
- require File.expand_path("#{@config[:require]}/config/environment.rb")
301
+ if ::Rails::VERSION::MAJOR < 6
302
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
287
303
  end
304
+ require "sidekiq/rails"
305
+ require File.expand_path("#{@config[:require]}/config/environment.rb")
288
306
  @config[:tag] ||= default_tag
289
307
  else
290
308
  require @config[:require]
@@ -332,10 +350,6 @@ module Sidekiq # :nodoc:
332
350
  opts[:concurrency] = Integer(arg)
333
351
  end
334
352
 
335
- o.on "-d", "--daemon", "Daemonize process" do |arg|
336
- puts "ERROR: Daemonization mode was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
337
- end
338
-
339
353
  o.on "-e", "--environment ENV", "Application environment" do |arg|
340
354
  opts[:environment] = arg
341
355
  end
@@ -345,8 +359,8 @@ module Sidekiq # :nodoc:
345
359
  end
346
360
 
347
361
  o.on "-q", "--queue QUEUE[,WEIGHT]", "Queues to process with optional weights" do |arg|
348
- queue, weight = arg.split(",")
349
- parse_queue opts, queue, weight
362
+ opts[:queues] ||= []
363
+ opts[:queues] << arg
350
364
  end
351
365
 
352
366
  o.on "-r", "--require [PATH|DIR]", "Location of Rails application with jobs or file to require" do |arg|
@@ -365,15 +379,7 @@ module Sidekiq # :nodoc:
365
379
  opts[:config_file] = arg
366
380
  end
367
381
 
368
- o.on "-L", "--logfile PATH", "path to writable logfile" do |arg|
369
- puts "ERROR: Logfile redirection was removed in Sidekiq 6.0, Sidekiq will only log to STDOUT"
370
- end
371
-
372
- o.on "-P", "--pidfile PATH", "path to pidfile" do |arg|
373
- puts "ERROR: PID file creation was removed in Sidekiq 6.0, please use a proper process supervisor to start and manage your services"
374
- end
375
-
376
- o.on "-V", "--version", "Print version and exit" do |arg|
382
+ o.on "-V", "--version", "Print version and exit" do
377
383
  puts "Sidekiq #{Sidekiq::VERSION}"
378
384
  die(0)
379
385
  end
@@ -393,9 +399,9 @@ module Sidekiq # :nodoc:
393
399
  end
394
400
 
395
401
  def parse_config(path)
396
- erb = ERB.new(File.read(path))
402
+ erb = ERB.new(File.read(path), trim_mode: "-")
397
403
  erb.filename = File.expand_path(path)
398
- opts = load_yaml(erb.result) || {}
404
+ opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
399
405
 
400
406
  if opts.respond_to? :deep_symbolize_keys!
401
407
  opts.deep_symbolize_keys!
@@ -406,31 +412,9 @@ module Sidekiq # :nodoc:
406
412
  opts = opts.merge(opts.delete(environment.to_sym) || {})
407
413
  opts.delete(:strict)
408
414
 
409
- parse_queues(opts, opts.delete(:queues) || [])
410
-
411
415
  opts
412
416
  end
413
417
 
414
- def load_yaml(src)
415
- if Psych::VERSION > "4.0"
416
- YAML.safe_load(src, permitted_classes: [Symbol], aliases: true)
417
- else
418
- YAML.load(src)
419
- end
420
- end
421
-
422
- def parse_queues(opts, queues_and_weights)
423
- queues_and_weights.each { |queue_and_weight| parse_queue(opts, *queue_and_weight) }
424
- end
425
-
426
- def parse_queue(opts, queue, weight = nil)
427
- opts[:queues] ||= []
428
- opts[:strict] = true if opts[:strict].nil?
429
- raise ArgumentError, "queues: #{queue} cannot be defined twice" if opts[:queues].include?(queue)
430
- [weight.to_i, 1].max.times { opts[:queues] << queue.to_s }
431
- opts[:strict] = false if weight.to_i > 0
432
- end
433
-
434
418
  def rails_app?
435
419
  defined?(::Rails) && ::Rails.respond_to?(:application)
436
420
  end
@@ -438,4 +422,4 @@ module Sidekiq # :nodoc:
438
422
  end
439
423
 
440
424
  require "sidekiq/systemd"
441
- require "sidekiq/metrics/tracking" if ENV["SIDEKIQ_METRICS_BETA"]
425
+ require "sidekiq/metrics/tracking"
@@ -21,7 +21,6 @@ module Sidekiq
21
21
  # Sidekiq.client_middleware but you can change as necessary.
22
22
  #
23
23
  def middleware(&block)
24
- @chain ||= Sidekiq.client_middleware
25
24
  if block
26
25
  @chain = @chain.dup
27
26
  yield @chain
@@ -31,18 +30,32 @@ module Sidekiq
31
30
 
32
31
  attr_accessor :redis_pool
33
32
 
34
- # Sidekiq::Client normally uses the default Redis pool but you may
35
- # pass a custom ConnectionPool if you want to shard your
36
- # Sidekiq jobs across several Redis instances (for scalability
37
- # reasons, e.g.)
33
+ # Sidekiq::Client is responsible for pushing job payloads to Redis.
34
+ # Requires the :pool or :config keyword argument.
38
35
  #
39
- # Sidekiq::Client.new(ConnectionPool.new { Redis.new })
36
+ # Sidekiq::Client.new(pool: Sidekiq::RedisConnection.create)
40
37
  #
41
- # Generally this is only needed for very large Sidekiq installs processing
42
- # thousands of jobs per second. I don't recommend sharding unless you
43
- # cannot scale any other way (e.g. splitting your app into smaller apps).
44
- def initialize(redis_pool = nil)
45
- @redis_pool = redis_pool || Thread.current[:sidekiq_via_pool] || Sidekiq.redis_pool
38
+ # Inside the Sidekiq process, you can reuse the configured resources:
39
+ #
40
+ # Sidekiq::Client.new(config: config)
41
+ #
42
+ # @param pool [ConnectionPool] explicit Redis pool to use
43
+ # @param config [Sidekiq::Config] use the pool and middleware from the given Sidekiq container
44
+ # @param chain [Sidekiq::Middleware::Chain] use the given middleware chain
45
+ def initialize(*args, **kwargs)
46
+ if args.size == 1 && kwargs.size == 0
47
+ warn "Sidekiq::Client.new(pool) is deprecated, please use Sidekiq::Client.new(pool: pool), #{caller(0..3)}"
48
+ # old calling method, accept 1 pool argument
49
+ @redis_pool = args[0]
50
+ @chain = Sidekiq.default_configuration.client_middleware
51
+ @config = Sidekiq.default_configuration
52
+ else
53
+ # new calling method: keyword arguments
54
+ @config = kwargs[:config] || Sidekiq.default_configuration
55
+ @redis_pool = kwargs[:pool] || Thread.current[:sidekiq_redis_pool] || @config&.redis_pool
56
+ @chain = kwargs[:chain] || @config&.client_middleware
57
+ raise ArgumentError, "No Redis pool available for Sidekiq::Client" unless @redis_pool
58
+ end
46
59
  end
47
60
 
48
61
  ##
@@ -53,6 +66,7 @@ module Sidekiq
53
66
  # args - an array of simple arguments to the perform method, must be JSON-serializable
54
67
  # at - timestamp to schedule the job (optional), must be Numeric (e.g. Time.now.to_f)
55
68
  # retry - whether to retry this job if it fails, default true or an integer number of retries
69
+ # retry_for - relative amount of time to retry this job if it fails, default nil
56
70
  # backtrace - whether to save any error backtrace, default false
57
71
  #
58
72
  # If class is set to the class name, the jobs' options will be based on Sidekiq's default
@@ -60,7 +74,7 @@ module Sidekiq
60
74
  #
61
75
  # Any options valid for a job class's sidekiq_options are also available here.
62
76
  #
63
- # All options must be strings, not symbols. NB: because we are serializing to JSON, all
77
+ # All keys must be strings, not symbols. NB: because we are serializing to JSON, all
64
78
  # symbols in 'args' will be converted to strings. Note that +backtrace: true+ can take quite a bit of
65
79
  # space in Redis; a large volume of failing jobs can start Redis swapping if you aren't careful.
66
80
  #
@@ -83,8 +97,9 @@ module Sidekiq
83
97
 
84
98
  ##
85
99
  # Push a large number of jobs to Redis. This method cuts out the redis
86
- # network round trip latency. I wouldn't recommend pushing more than
87
- # 1000 per call but YMMV based on network quality, size of job args, etc.
100
+ # network round trip latency. It pushes jobs in batches if more than
101
+ # `:batch_size` (1000 by default) of jobs are passed. I wouldn't recommend making `:batch_size`
102
+ # larger than 1000 but YMMV based on network quality, size of job args, etc.
88
103
  # A large number of jobs can cause a bit of Redis command processing latency.
89
104
  #
90
105
  # Takes the same arguments as #push except that args is expected to be
@@ -92,13 +107,15 @@ module Sidekiq
92
107
  # is run through the client middleware pipeline and each job gets its own Job ID
93
108
  # as normal.
94
109
  #
95
- # Returns an array of the of pushed jobs' jids. The number of jobs pushed can be less
96
- # than the number given if the middleware stopped processing for one or more jobs.
110
+ # Returns an array of the of pushed jobs' jids, may contain nils if any client middleware
111
+ # prevented a job push.
112
+ #
113
+ # Example (pushing jobs in batches):
114
+ # push_bulk('class' => MyJob, 'args' => (1..100_000).to_a, batch_size: 1_000)
115
+ #
97
116
  def push_bulk(items)
117
+ batch_size = items.delete(:batch_size) || items.delete("batch_size") || 1_000
98
118
  args = items["args"]
99
- raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless args.is_a?(Array) && args.all?(Array)
100
- return [] if args.empty? # no jobs to push
101
-
102
119
  at = items.delete("at")
103
120
  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) })
104
121
  raise ArgumentError, "Job 'at' Array must have same size as 'args' Array" if at.is_a?(Array) && at.size != args.size
@@ -107,18 +124,28 @@ module Sidekiq
107
124
  raise ArgumentError, "Explicitly passing 'jid' when pushing more than one job is not supported" if jid && args.size > 1
108
125
 
109
126
  normed = normalize_item(items)
110
- payloads = args.map.with_index { |job_args, index|
111
- copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12))
112
- copy["at"] = (at.is_a?(Array) ? at[index] : at) if at
113
- result = middleware.invoke(items["class"], copy, copy["queue"], @redis_pool) do
114
- verify_json(copy)
115
- copy
116
- end
117
- result || nil
118
- }.compact
127
+ slice_index = 0
128
+ result = args.each_slice(batch_size).flat_map do |slice|
129
+ raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[1], [2]]" unless slice.is_a?(Array) && slice.all?(Array)
130
+ break [] if slice.empty? # no jobs to push
131
+
132
+ payloads = slice.map.with_index { |job_args, index|
133
+ copy = normed.merge("args" => job_args, "jid" => SecureRandom.hex(12))
134
+ copy["at"] = (at.is_a?(Array) ? at[slice_index + index] : at) if at
135
+ result = middleware.invoke(items["class"], copy, copy["queue"], @redis_pool) do
136
+ verify_json(copy)
137
+ copy
138
+ end
139
+ result || nil
140
+ }
141
+ slice_index += batch_size
142
+
143
+ to_push = payloads.compact
144
+ raw_push(to_push) unless to_push.empty?
145
+ payloads.map { |payload| payload&.[]("jid") }
146
+ end
119
147
 
120
- raw_push(payloads) unless payloads.empty?
121
- payloads.collect { |payload| payload["jid"] }
148
+ result.is_a?(Enumerator::Lazy) ? result.force : result
122
149
  end
123
150
 
124
151
  # Allows sharding of jobs across any number of Redis instances. All jobs
@@ -135,11 +162,11 @@ module Sidekiq
135
162
  # you cannot scale any other way (e.g. splitting your app into smaller apps).
136
163
  def self.via(pool)
137
164
  raise ArgumentError, "No pool given" if pool.nil?
138
- current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
139
- Thread.current[:sidekiq_via_pool] = pool
165
+ current_sidekiq_pool = Thread.current[:sidekiq_redis_pool]
166
+ Thread.current[:sidekiq_redis_pool] = pool
140
167
  yield
141
168
  ensure
142
- Thread.current[:sidekiq_via_pool] = current_sidekiq_pool
169
+ Thread.current[:sidekiq_redis_pool] = current_sidekiq_pool
143
170
  end
144
171
 
145
172
  class << self
@@ -147,8 +174,8 @@ module Sidekiq
147
174
  new.push(item)
148
175
  end
149
176
 
150
- def push_bulk(items)
151
- new.push_bulk(items)
177
+ def push_bulk(...)
178
+ new.push_bulk(...)
152
179
  end
153
180
 
154
181
  # Resque compatibility helpers. Note all helpers
@@ -201,14 +228,14 @@ module Sidekiq
201
228
  conn.pipelined do |pipeline|
202
229
  atomic_push(pipeline, payloads)
203
230
  end
204
- rescue RedisConnection.adapter::BaseError => ex
231
+ rescue RedisClient::Error => ex
205
232
  # 2550 Failover can cause the server to become a replica, need
206
233
  # to disconnect and reopen the socket to get back to the primary.
207
234
  # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
208
235
  # 4985 Use the same logic when a blocking command is force-unblocked
209
236
  # The retry logic is copied from sidekiq.rb
210
237
  if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
211
- conn.disconnect!
238
+ conn.close
212
239
  retryable = false
213
240
  retry
214
241
  end
@@ -222,6 +249,8 @@ module Sidekiq
222
249
  if payloads.first.key?("at")
223
250
  conn.zadd("schedule", payloads.flat_map { |hash|
224
251
  at = hash.delete("at").to_s
252
+ # ActiveJob sets this but the job has not been enqueued yet
253
+ hash.delete("enqueued_at")
225
254
  [at, Sidekiq.dump_json(hash)]
226
255
  })
227
256
  else
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sidekiq
2
4
  ##
3
5
  # Sidekiq::Component assumes a config instance is available at @config
@@ -13,7 +15,7 @@ module Sidekiq
13
15
 
14
16
  def safe_thread(name, &block)
15
17
  Thread.new do
16
- Thread.current.name = name
18
+ Thread.current.name = "sidekiq.#{name}"
17
19
  watchdog(name, &block)
18
20
  end
19
21
  end
@@ -50,6 +52,7 @@ module Sidekiq
50
52
  oneshot = options.fetch(:oneshot, true)
51
53
  reverse = options[:reverse]
52
54
  reraise = options[:reraise]
55
+ logger.debug("Firing #{event} event") if oneshot
53
56
 
54
57
  arr = config[:lifecycle_events][event]
55
58
  arr.reverse! if reverse