sidekiq 6.5.9 → 7.0.9

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +93 -17
  3. data/README.md +40 -32
  4. data/bin/sidekiq +3 -8
  5. data/bin/sidekiqload +186 -118
  6. data/bin/sidekiqmon +3 -0
  7. data/lib/sidekiq/api.rb +84 -121
  8. data/lib/sidekiq/capsule.rb +127 -0
  9. data/lib/sidekiq/cli.rb +55 -74
  10. data/lib/sidekiq/client.rb +29 -16
  11. data/lib/sidekiq/component.rb +3 -0
  12. data/lib/sidekiq/config.rb +270 -0
  13. data/lib/sidekiq/deploy.rb +62 -0
  14. data/lib/sidekiq/embedded.rb +61 -0
  15. data/lib/sidekiq/fetch.rb +11 -14
  16. data/lib/sidekiq/job.rb +375 -10
  17. data/lib/sidekiq/job_logger.rb +2 -2
  18. data/lib/sidekiq/job_retry.rb +13 -12
  19. data/lib/sidekiq/job_util.rb +48 -14
  20. data/lib/sidekiq/launcher.rb +65 -61
  21. data/lib/sidekiq/logger.rb +1 -26
  22. data/lib/sidekiq/manager.rb +9 -11
  23. data/lib/sidekiq/metrics/query.rb +3 -3
  24. data/lib/sidekiq/metrics/shared.rb +7 -6
  25. data/lib/sidekiq/metrics/tracking.rb +20 -18
  26. data/lib/sidekiq/middleware/chain.rb +19 -18
  27. data/lib/sidekiq/middleware/current_attributes.rb +8 -15
  28. data/lib/sidekiq/monitor.rb +16 -3
  29. data/lib/sidekiq/processor.rb +21 -27
  30. data/lib/sidekiq/rails.rb +4 -9
  31. data/lib/sidekiq/redis_client_adapter.rb +8 -47
  32. data/lib/sidekiq/redis_connection.rb +11 -111
  33. data/lib/sidekiq/scheduled.rb +20 -21
  34. data/lib/sidekiq/testing.rb +5 -33
  35. data/lib/sidekiq/transaction_aware_client.rb +4 -5
  36. data/lib/sidekiq/version.rb +2 -1
  37. data/lib/sidekiq/web/application.rb +21 -6
  38. data/lib/sidekiq/web/csrf_protection.rb +1 -1
  39. data/lib/sidekiq/web/helpers.rb +16 -15
  40. data/lib/sidekiq/web.rb +6 -17
  41. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  42. data/lib/sidekiq.rb +76 -274
  43. data/sidekiq.gemspec +21 -10
  44. data/web/assets/javascripts/application.js +18 -0
  45. data/web/assets/javascripts/base-charts.js +106 -0
  46. data/web/assets/javascripts/dashboard-charts.js +166 -0
  47. data/web/assets/javascripts/dashboard.js +3 -223
  48. data/web/assets/javascripts/metrics.js +117 -115
  49. data/web/assets/stylesheets/application-dark.css +4 -0
  50. data/web/assets/stylesheets/application-rtl.css +2 -91
  51. data/web/assets/stylesheets/application.css +23 -298
  52. data/web/locales/ar.yml +70 -70
  53. data/web/locales/cs.yml +62 -62
  54. data/web/locales/da.yml +60 -53
  55. data/web/locales/de.yml +65 -65
  56. data/web/locales/el.yml +2 -7
  57. data/web/locales/en.yml +76 -70
  58. data/web/locales/es.yml +68 -68
  59. data/web/locales/fa.yml +65 -65
  60. data/web/locales/fr.yml +67 -67
  61. data/web/locales/gd.yml +99 -0
  62. data/web/locales/he.yml +65 -64
  63. data/web/locales/hi.yml +59 -59
  64. data/web/locales/it.yml +53 -53
  65. data/web/locales/ja.yml +67 -69
  66. data/web/locales/ko.yml +52 -52
  67. data/web/locales/lt.yml +66 -66
  68. data/web/locales/nb.yml +61 -61
  69. data/web/locales/nl.yml +52 -52
  70. data/web/locales/pl.yml +45 -45
  71. data/web/locales/pt-br.yml +59 -69
  72. data/web/locales/pt.yml +51 -51
  73. data/web/locales/ru.yml +67 -66
  74. data/web/locales/sv.yml +53 -53
  75. data/web/locales/ta.yml +60 -60
  76. data/web/locales/uk.yml +62 -61
  77. data/web/locales/ur.yml +64 -64
  78. data/web/locales/vi.yml +67 -67
  79. data/web/locales/zh-cn.yml +20 -18
  80. data/web/locales/zh-tw.yml +10 -1
  81. data/web/views/_footer.erb +5 -2
  82. data/web/views/_job_info.erb +18 -2
  83. data/web/views/_metrics_period_select.erb +12 -0
  84. data/web/views/_paging.erb +2 -0
  85. data/web/views/_poll_link.erb +1 -1
  86. data/web/views/busy.erb +39 -28
  87. data/web/views/dashboard.erb +36 -5
  88. data/web/views/metrics.erb +33 -20
  89. data/web/views/metrics_for_job.erb +24 -43
  90. data/web/views/morgue.erb +5 -9
  91. data/web/views/queue.erb +10 -14
  92. data/web/views/queues.erb +3 -1
  93. data/web/views/retries.erb +5 -9
  94. data/web/views/scheduled.erb +12 -13
  95. metadata +51 -39
  96. data/lib/sidekiq/delay.rb +0 -43
  97. data/lib/sidekiq/extensions/action_mailer.rb +0 -48
  98. data/lib/sidekiq/extensions/active_record.rb +0 -43
  99. data/lib/sidekiq/extensions/class_methods.rb +0 -43
  100. data/lib/sidekiq/extensions/generic_proxy.rb +0 -33
  101. data/lib/sidekiq/metrics/deploy.rb +0 -47
  102. data/lib/sidekiq/worker.rb +0 -370
  103. data/web/assets/javascripts/graph.js +0 -16
  104. /data/{LICENSE → LICENSE.txt} +0 -0
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
@@ -52,7 +41,7 @@ module Sidekiq # :nodoc:
52
41
  def run(boot_app: 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
@@ -115,8 +105,8 @@ module Sidekiq # :nodoc:
115
105
  # Starting here the process will now have multiple threads running.
116
106
  fire_event(:startup, reverse: false, reraise: true)
117
107
 
118
- logger.debug { "Client Middleware: #{@config.client_middleware.map(&:klass).join(", ")}" }
119
- logger.debug { "Server Middleware: #{@config.server_middleware.map(&:klass).join(", ")}" }
108
+ logger.debug { "Client Middleware: #{@config.default_capsule.client_middleware.map(&:klass).join(", ")}" }
109
+ logger.debug { "Server Middleware: #{@config.default_capsule.server_middleware.map(&:klass).join(", ")}" }
120
110
 
121
111
  launch(self_read)
122
112
  end
@@ -149,19 +139,34 @@ module Sidekiq # :nodoc:
149
139
  end
150
140
  end
151
141
 
152
- def self.w
153
- "\e[37m"
142
+ HOLIDAY_COLORS = {
143
+ # got other color-specific holidays from around the world?
144
+ # https://developer-book.com/post/definitive-guide-for-colored-text-in-terminal/#256-color-escape-codes
145
+ "3-17" => "\e[1;32m", # St. Patrick's Day green
146
+ "10-31" => "\e[38;5;208m" # Halloween orange
147
+ }
148
+
149
+ def self.day
150
+ @@day ||= begin
151
+ t = Date.today
152
+ "#{t.month}-#{t.day}"
153
+ end
154
154
  end
155
155
 
156
156
  def self.r
157
- "\e[31m"
157
+ @@r ||= HOLIDAY_COLORS[day] || "\e[1;31m"
158
158
  end
159
159
 
160
160
  def self.b
161
- "\e[30m"
161
+ @@b ||= HOLIDAY_COLORS[day] || "\e[30m"
162
+ end
163
+
164
+ def self.w
165
+ "\e[1;37m"
162
166
  end
163
167
 
164
168
  def self.reset
169
+ @@b = @@r = @@day = nil
165
170
  "\e[0m"
166
171
  end
167
172
 
@@ -174,7 +179,7 @@ module Sidekiq # :nodoc:
174
179
  #{w} ,$$$$$b#{b}/#{w}md$$$P^'
175
180
  #{w} .d$$$$$$#{b}/#{w}$$$P'
176
181
  #{w} $$^' `"#{b}/#{w}$$$' #{r}____ _ _ _ _
177
- #{w} $: ,$$: #{r} / ___|(_) __| | ___| | _(_) __ _
182
+ #{w} $: #{b}'#{w},$$: #{r} / ___|(_) __| | ___| | _(_) __ _
178
183
  #{w} `b :$$ #{r} \\___ \\| |/ _` |/ _ \\ |/ / |/ _` |
179
184
  #{w} $$: #{r} ___) | | (_| | __/ <| | (_| |
180
185
  #{w} $$ #{r}|____/|_|\\__,_|\\___|_|\\_\\_|\\__, |
@@ -225,7 +230,6 @@ module Sidekiq # :nodoc:
225
230
  # Both Sinatra 2.0+ and Sidekiq support this term.
226
231
  # RAILS_ENV and RACK_ENV are there for legacy support.
227
232
  @environment = cli_env || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
228
- config[:environment] = @environment
229
233
  end
230
234
 
231
235
  def symbolize_keys_deep!(hash)
@@ -272,6 +276,18 @@ module Sidekiq # :nodoc:
272
276
 
273
277
  # merge with defaults
274
278
  @config.merge!(opts)
279
+
280
+ @config.default_capsule.tap do |cap|
281
+ cap.queues = opts[:queues]
282
+ cap.concurrency = opts[:concurrency] || @config[:concurrency]
283
+ end
284
+
285
+ opts[:capsules]&.each do |name, cap_config|
286
+ @config.capsule(name.to_s) do |cap|
287
+ cap.queues = cap_config[:queues]
288
+ cap.concurrency = cap_config[:concurrency]
289
+ end
290
+ end
275
291
  end
276
292
 
277
293
  def boot_application
@@ -279,12 +295,11 @@ module Sidekiq # :nodoc:
279
295
 
280
296
  if File.directory?(@config[:require])
281
297
  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")
298
+ if ::Rails::VERSION::MAJOR < 6
299
+ warn "Sidekiq #{Sidekiq::VERSION} only supports Rails 6+"
287
300
  end
301
+ require "sidekiq/rails"
302
+ require File.expand_path("#{@config[:require]}/config/environment.rb")
288
303
  @config[:tag] ||= default_tag
289
304
  else
290
305
  require @config[:require]
@@ -332,10 +347,6 @@ module Sidekiq # :nodoc:
332
347
  opts[:concurrency] = Integer(arg)
333
348
  end
334
349
 
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
350
  o.on "-e", "--environment ENV", "Application environment" do |arg|
340
351
  opts[:environment] = arg
341
352
  end
@@ -345,8 +356,8 @@ module Sidekiq # :nodoc:
345
356
  end
346
357
 
347
358
  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
359
+ opts[:queues] ||= []
360
+ opts[:queues] << arg
350
361
  end
351
362
 
352
363
  o.on "-r", "--require [PATH|DIR]", "Location of Rails application with jobs or file to require" do |arg|
@@ -365,15 +376,7 @@ module Sidekiq # :nodoc:
365
376
  opts[:config_file] = arg
366
377
  end
367
378
 
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|
379
+ o.on "-V", "--version", "Print version and exit" do
377
380
  puts "Sidekiq #{Sidekiq::VERSION}"
378
381
  die(0)
379
382
  end
@@ -395,7 +398,7 @@ module Sidekiq # :nodoc:
395
398
  def parse_config(path)
396
399
  erb = ERB.new(File.read(path))
397
400
  erb.filename = File.expand_path(path)
398
- opts = load_yaml(erb.result) || {}
401
+ opts = YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true) || {}
399
402
 
400
403
  if opts.respond_to? :deep_symbolize_keys!
401
404
  opts.deep_symbolize_keys!
@@ -406,31 +409,9 @@ module Sidekiq # :nodoc:
406
409
  opts = opts.merge(opts.delete(environment.to_sym) || {})
407
410
  opts.delete(:strict)
408
411
 
409
- parse_queues(opts, opts.delete(:queues) || [])
410
-
411
412
  opts
412
413
  end
413
414
 
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
415
  def rails_app?
435
416
  defined?(::Rails) && ::Rails.respond_to?(:application)
436
417
  end
@@ -438,4 +419,4 @@ module Sidekiq # :nodoc:
438
419
  end
439
420
 
440
421
  require "sidekiq/systemd"
441
- require "sidekiq/metrics/tracking" if ENV["SIDEKIQ_METRICS_BETA"]
422
+ 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
  ##
@@ -135,11 +148,11 @@ module Sidekiq
135
148
  # you cannot scale any other way (e.g. splitting your app into smaller apps).
136
149
  def self.via(pool)
137
150
  raise ArgumentError, "No pool given" if pool.nil?
138
- current_sidekiq_pool = Thread.current[:sidekiq_via_pool]
139
- Thread.current[:sidekiq_via_pool] = pool
151
+ current_sidekiq_pool = Thread.current[:sidekiq_redis_pool]
152
+ Thread.current[:sidekiq_redis_pool] = pool
140
153
  yield
141
154
  ensure
142
- Thread.current[:sidekiq_via_pool] = current_sidekiq_pool
155
+ Thread.current[:sidekiq_redis_pool] = current_sidekiq_pool
143
156
  end
144
157
 
145
158
  class << self
@@ -201,14 +214,14 @@ module Sidekiq
201
214
  conn.pipelined do |pipeline|
202
215
  atomic_push(pipeline, payloads)
203
216
  end
204
- rescue RedisConnection.adapter::BaseError => ex
217
+ rescue RedisClient::Error => ex
205
218
  # 2550 Failover can cause the server to become a replica, need
206
219
  # to disconnect and reopen the socket to get back to the primary.
207
220
  # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
208
221
  # 4985 Use the same logic when a blocking command is force-unblocked
209
222
  # The retry logic is copied from sidekiq.rb
210
223
  if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
211
- conn.disconnect!
224
+ conn.close
212
225
  retryable = false
213
226
  retry
214
227
  end
@@ -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
@@ -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
@@ -0,0 +1,270 @@
1
+ require "forwardable"
2
+
3
+ require "set"
4
+ require "sidekiq/redis_connection"
5
+
6
+ module Sidekiq
7
+ # Sidekiq::Config represents the global configuration for an instance of Sidekiq.
8
+ class Config
9
+ extend Forwardable
10
+
11
+ DEFAULTS = {
12
+ labels: Set.new,
13
+ require: ".",
14
+ environment: nil,
15
+ concurrency: 5,
16
+ timeout: 25,
17
+ poll_interval_average: nil,
18
+ average_scheduled_poll_interval: 5,
19
+ on_complex_arguments: :raise,
20
+ error_handlers: [],
21
+ death_handlers: [],
22
+ lifecycle_events: {
23
+ startup: [],
24
+ quiet: [],
25
+ shutdown: [],
26
+ # triggers when we fire the first heartbeat on startup OR repairing a network partition
27
+ heartbeat: [],
28
+ # triggers on EVERY heartbeat call, every 10 seconds
29
+ beat: []
30
+ },
31
+ dead_max_jobs: 10_000,
32
+ dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
33
+ reloader: proc { |&block| block.call }
34
+ }
35
+
36
+ ERROR_HANDLER = ->(ex, ctx) {
37
+ cfg = ctx[:_config] || Sidekiq.default_configuration
38
+ l = cfg.logger
39
+ l.warn(Sidekiq.dump_json(ctx)) unless ctx.empty?
40
+ l.warn("#{ex.class.name}: #{ex.message}")
41
+ l.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
42
+ }
43
+
44
+ def initialize(options = {})
45
+ @options = DEFAULTS.merge(options)
46
+ @options[:error_handlers] << ERROR_HANDLER if @options[:error_handlers].empty?
47
+ @directory = {}
48
+ @redis_config = {}
49
+ @capsules = {}
50
+ end
51
+
52
+ def_delegators :@options, :[], :[]=, :fetch, :key?, :has_key?, :merge!
53
+ attr_reader :capsules
54
+
55
+ # LEGACY: edits the default capsule
56
+ # config.concurrency = 5
57
+ def concurrency=(val)
58
+ default_capsule.concurrency = Integer(val)
59
+ end
60
+
61
+ def concurrency
62
+ default_capsule.concurrency
63
+ end
64
+
65
+ def total_concurrency
66
+ capsules.each_value.sum(&:concurrency)
67
+ end
68
+
69
+ # Edit the default capsule.
70
+ # config.queues = %w( high default low ) # strict
71
+ # config.queues = %w( high,3 default,2 low,1 ) # weighted
72
+ # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
73
+ #
74
+ # With weighted priority, queue will be checked first (weight / total) of the time.
75
+ # high will be checked first (3/6) or 50% of the time.
76
+ # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
77
+ # are ridiculous and unnecessarily expensive. You can get random queue ordering
78
+ # by explicitly setting all weights to 1.
79
+ def queues=(val)
80
+ default_capsule.queues = val
81
+ end
82
+
83
+ def queues
84
+ default_capsule.queues
85
+ end
86
+
87
+ def client_middleware
88
+ @client_chain ||= Sidekiq::Middleware::Chain.new(self)
89
+ yield @client_chain if block_given?
90
+ @client_chain
91
+ end
92
+
93
+ def server_middleware
94
+ @server_chain ||= Sidekiq::Middleware::Chain.new(self)
95
+ yield @server_chain if block_given?
96
+ @server_chain
97
+ end
98
+
99
+ def default_capsule(&block)
100
+ capsule("default", &block)
101
+ end
102
+
103
+ # register a new queue processing subsystem
104
+ def capsule(name)
105
+ nm = name.to_s
106
+ cap = @capsules.fetch(nm) do
107
+ cap = Sidekiq::Capsule.new(nm, self)
108
+ @capsules[nm] = cap
109
+ end
110
+ yield cap if block_given?
111
+ cap
112
+ end
113
+
114
+ # All capsules must use the same Redis configuration
115
+ def redis=(hash)
116
+ @redis_config = @redis_config.merge(hash)
117
+ end
118
+
119
+ def redis_pool
120
+ Thread.current[:sidekiq_redis_pool] || Thread.current[:sidekiq_capsule]&.redis_pool || local_redis_pool
121
+ end
122
+
123
+ private def local_redis_pool
124
+ # this is our internal client/housekeeping pool. each capsule has its
125
+ # own pool for executing threads.
126
+ @redis ||= new_redis_pool(5, "internal")
127
+ end
128
+
129
+ def new_redis_pool(size, name = "unset")
130
+ # connection pool is lazy, it will not create connections unless you actually need them
131
+ # so don't be skimpy!
132
+ RedisConnection.create({size: size, logger: logger, pool_name: name}.merge(@redis_config))
133
+ end
134
+
135
+ def redis_info
136
+ redis do |conn|
137
+ conn.call("INFO") { |i| i.lines(chomp: true).map { |l| l.split(":", 2) }.select { |l| l.size == 2 }.to_h }
138
+ rescue RedisClientAdapter::CommandError => ex
139
+ # 2850 return fake version when INFO command has (probably) been renamed
140
+ raise unless /unknown command/.match?(ex.message)
141
+ {
142
+ "redis_version" => "9.9.9",
143
+ "uptime_in_days" => "9999",
144
+ "connected_clients" => "9999",
145
+ "used_memory_human" => "9P",
146
+ "used_memory_peak_human" => "9P"
147
+ }.freeze
148
+ end
149
+ end
150
+
151
+ def redis
152
+ raise ArgumentError, "requires a block" unless block_given?
153
+ redis_pool.with do |conn|
154
+ retryable = true
155
+ begin
156
+ yield conn
157
+ rescue RedisClientAdapter::BaseError => ex
158
+ # 2550 Failover can cause the server to become a replica, need
159
+ # to disconnect and reopen the socket to get back to the primary.
160
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
161
+ # 4985 Use the same logic when a blocking command is force-unblocked
162
+ # The same retry logic is also used in client.rb
163
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
164
+ conn.close
165
+ retryable = false
166
+ retry
167
+ end
168
+ raise
169
+ end
170
+ end
171
+ end
172
+
173
+ # register global singletons which can be accessed elsewhere
174
+ def register(name, instance)
175
+ @directory[name] = instance
176
+ end
177
+
178
+ # find a singleton
179
+ def lookup(name, default_class = nil)
180
+ # JNDI is just a fancy name for a hash lookup
181
+ @directory.fetch(name) do |key|
182
+ return nil unless default_class
183
+ @directory[key] = default_class.new(self)
184
+ end
185
+ end
186
+
187
+ ##
188
+ # Death handlers are called when all retries for a job have been exhausted and
189
+ # the job dies. It's the notification to your application
190
+ # that this job will not succeed without manual intervention.
191
+ #
192
+ # Sidekiq.configure_server do |config|
193
+ # config.death_handlers << ->(job, ex) do
194
+ # end
195
+ # end
196
+ def death_handlers
197
+ @options[:death_handlers]
198
+ end
199
+
200
+ # How frequently Redis should be checked by a random Sidekiq process for
201
+ # scheduled and retriable jobs. Each individual process will take turns by
202
+ # waiting some multiple of this value.
203
+ #
204
+ # See sidekiq/scheduled.rb for an in-depth explanation of this value
205
+ def average_scheduled_poll_interval=(interval)
206
+ @options[:average_scheduled_poll_interval] = interval
207
+ end
208
+
209
+ # Register a proc to handle any error which occurs within the Sidekiq process.
210
+ #
211
+ # Sidekiq.configure_server do |config|
212
+ # config.error_handlers << proc {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
213
+ # end
214
+ #
215
+ # The default error handler logs errors to @logger.
216
+ def error_handlers
217
+ @options[:error_handlers]
218
+ end
219
+
220
+ # Register a block to run at a point in the Sidekiq lifecycle.
221
+ # :startup, :quiet or :shutdown are valid events.
222
+ #
223
+ # Sidekiq.configure_server do |config|
224
+ # config.on(:shutdown) do
225
+ # puts "Goodbye cruel world!"
226
+ # end
227
+ # end
228
+ def on(event, &block)
229
+ raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
230
+ raise ArgumentError, "Invalid event name: #{event}" unless @options[:lifecycle_events].key?(event)
231
+ @options[:lifecycle_events][event] << block
232
+ end
233
+
234
+ def logger
235
+ @logger ||= Sidekiq::Logger.new($stdout, level: :info).tap do |log|
236
+ log.level = Logger::INFO
237
+ log.formatter = if ENV["DYNO"]
238
+ Sidekiq::Logger::Formatters::WithoutTimestamp.new
239
+ else
240
+ Sidekiq::Logger::Formatters::Pretty.new
241
+ end
242
+ end
243
+ end
244
+
245
+ def logger=(logger)
246
+ if logger.nil?
247
+ self.logger.level = Logger::FATAL
248
+ return
249
+ end
250
+
251
+ @logger = logger
252
+ end
253
+
254
+ # INTERNAL USE ONLY
255
+ def handle_exception(ex, ctx = {})
256
+ if @options[:error_handlers].size == 0
257
+ p ["!!!!!", ex]
258
+ end
259
+ ctx[:_config] = self
260
+ @options[:error_handlers].each do |handler|
261
+ handler.call(ex, ctx)
262
+ rescue => e
263
+ l = logger
264
+ l.error "!!! ERROR HANDLER THREW AN ERROR !!!"
265
+ l.error e
266
+ l.error e.backtrace.join("\n") unless e.backtrace.nil?
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,62 @@
1
+ require "sidekiq/redis_connection"
2
+ require "time"
3
+
4
+ # This file is designed to be required within the user's
5
+ # deployment script; it should need a bare minimum of dependencies.
6
+ # Usage:
7
+ #
8
+ # require "sidekiq/deploy"
9
+ # Sidekiq::Deploy.mark!("Some change")
10
+ #
11
+ # If you do not pass a label, Sidekiq will try to use the latest
12
+ # git commit info.
13
+ #
14
+
15
+ module Sidekiq
16
+ class Deploy
17
+ MARK_TTL = 90 * 24 * 60 * 60 # 90 days
18
+
19
+ LABEL_MAKER = -> {
20
+ `git log -1 --format="%h %s"`.strip
21
+ }
22
+
23
+ def self.mark!(label = nil)
24
+ Sidekiq::Deploy.new.mark!(label: label)
25
+ end
26
+
27
+ def initialize(pool = Sidekiq::RedisConnection.create)
28
+ @pool = pool
29
+ end
30
+
31
+ def mark!(at: Time.now, label: nil)
32
+ label ||= LABEL_MAKER.call
33
+ # we need to round the timestamp so that we gracefully
34
+ # handle an very common error in marking deploys:
35
+ # having every process mark its deploy, leading
36
+ # to N marks for each deploy. Instead we round the time
37
+ # to the minute so that multple marks within that minute
38
+ # will all naturally rollup into one mark per minute.
39
+ whence = at.utc
40
+ floor = Time.utc(whence.year, whence.month, whence.mday, whence.hour, whence.min, 0)
41
+ datecode = floor.strftime("%Y%m%d")
42
+ key = "#{datecode}-marks"
43
+ stamp = floor.iso8601
44
+
45
+ @pool.with do |c|
46
+ # only allow one deploy mark for a given label for the next minute
47
+ lock = c.set("deploylock-#{label}", stamp, nx: true, ex: 60)
48
+ if lock
49
+ c.multi do |pipe|
50
+ pipe.hsetnx(key, stamp, label)
51
+ pipe.expire(key, MARK_TTL)
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def fetch(date = Time.now.utc.to_date)
58
+ datecode = date.strftime("%Y%m%d")
59
+ @pool.with { |c| c.hgetall("#{datecode}-marks") }
60
+ end
61
+ end
62
+ end