sidekiq 5.2.10 → 6.5.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +422 -1
  3. data/LICENSE +3 -3
  4. data/README.md +24 -35
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +79 -67
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +527 -310
  13. data/lib/sidekiq/cli.rb +204 -208
  14. data/lib/sidekiq/client.rb +78 -82
  15. data/lib/sidekiq/component.rb +65 -0
  16. data/lib/sidekiq/delay.rb +8 -7
  17. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  18. data/lib/sidekiq/extensions/active_record.rb +13 -10
  19. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  20. data/lib/sidekiq/extensions/generic_proxy.rb +7 -5
  21. data/lib/sidekiq/fetch.rb +50 -40
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +33 -7
  24. data/lib/sidekiq/job_retry.rb +126 -106
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +177 -83
  27. data/lib/sidekiq/logger.rb +156 -0
  28. data/lib/sidekiq/manager.rb +40 -41
  29. data/lib/sidekiq/metrics/deploy.rb +47 -0
  30. data/lib/sidekiq/metrics/query.rb +153 -0
  31. data/lib/sidekiq/metrics/shared.rb +94 -0
  32. data/lib/sidekiq/metrics/tracking.rb +134 -0
  33. data/lib/sidekiq/middleware/chain.rb +102 -46
  34. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  35. data/lib/sidekiq/middleware/i18n.rb +7 -7
  36. data/lib/sidekiq/middleware/modules.rb +21 -0
  37. data/lib/sidekiq/monitor.rb +133 -0
  38. data/lib/sidekiq/paginator.rb +28 -16
  39. data/lib/sidekiq/processor.rb +104 -97
  40. data/lib/sidekiq/rails.rb +46 -37
  41. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  42. data/lib/sidekiq/redis_connection.rb +108 -77
  43. data/lib/sidekiq/ring_buffer.rb +29 -0
  44. data/lib/sidekiq/scheduled.rb +105 -42
  45. data/lib/sidekiq/sd_notify.rb +149 -0
  46. data/lib/sidekiq/systemd.rb +24 -0
  47. data/lib/sidekiq/testing/inline.rb +6 -5
  48. data/lib/sidekiq/testing.rb +68 -58
  49. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  50. data/lib/sidekiq/version.rb +2 -1
  51. data/lib/sidekiq/web/action.rb +15 -11
  52. data/lib/sidekiq/web/application.rb +103 -77
  53. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  54. data/lib/sidekiq/web/helpers.rb +125 -95
  55. data/lib/sidekiq/web/router.rb +23 -19
  56. data/lib/sidekiq/web.rb +65 -105
  57. data/lib/sidekiq/worker.rb +259 -109
  58. data/lib/sidekiq.rb +170 -62
  59. data/sidekiq.gemspec +23 -16
  60. data/web/assets/images/apple-touch-icon.png +0 -0
  61. data/web/assets/javascripts/application.js +113 -61
  62. data/web/assets/javascripts/chart.min.js +13 -0
  63. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  64. data/web/assets/javascripts/dashboard.js +53 -89
  65. data/web/assets/javascripts/graph.js +16 -0
  66. data/web/assets/javascripts/metrics.js +262 -0
  67. data/web/assets/stylesheets/application-dark.css +143 -0
  68. data/web/assets/stylesheets/application-rtl.css +0 -4
  69. data/web/assets/stylesheets/application.css +88 -233
  70. data/web/locales/ar.yml +8 -2
  71. data/web/locales/de.yml +14 -2
  72. data/web/locales/el.yml +43 -19
  73. data/web/locales/en.yml +13 -1
  74. data/web/locales/es.yml +18 -2
  75. data/web/locales/fr.yml +10 -3
  76. data/web/locales/ja.yml +14 -1
  77. data/web/locales/lt.yml +83 -0
  78. data/web/locales/pl.yml +4 -4
  79. data/web/locales/pt-br.yml +27 -9
  80. data/web/locales/ru.yml +4 -0
  81. data/web/locales/vi.yml +83 -0
  82. data/web/locales/zh-cn.yml +36 -11
  83. data/web/locales/zh-tw.yml +32 -7
  84. data/web/views/_footer.erb +1 -1
  85. data/web/views/_job_info.erb +3 -2
  86. data/web/views/_nav.erb +1 -1
  87. data/web/views/_poll_link.erb +2 -5
  88. data/web/views/_summary.erb +7 -7
  89. data/web/views/busy.erb +61 -22
  90. data/web/views/dashboard.erb +23 -14
  91. data/web/views/dead.erb +3 -3
  92. data/web/views/layout.erb +3 -1
  93. data/web/views/metrics.erb +69 -0
  94. data/web/views/metrics_for_job.erb +87 -0
  95. data/web/views/morgue.erb +9 -6
  96. data/web/views/queue.erb +23 -10
  97. data/web/views/queues.erb +10 -2
  98. data/web/views/retries.erb +11 -8
  99. data/web/views/retry.erb +3 -3
  100. data/web/views/scheduled.erb +5 -2
  101. metadata +58 -63
  102. data/.circleci/config.yml +0 -61
  103. data/.github/contributing.md +0 -32
  104. data/.github/issue_template.md +0 -11
  105. data/.gitignore +0 -15
  106. data/.travis.yml +0 -11
  107. data/3.0-Upgrade.md +0 -70
  108. data/4.0-Upgrade.md +0 -53
  109. data/5.0-Upgrade.md +0 -56
  110. data/COMM-LICENSE +0 -97
  111. data/Ent-Changes.md +0 -238
  112. data/Gemfile +0 -19
  113. data/Pro-2.0-Upgrade.md +0 -138
  114. data/Pro-3.0-Upgrade.md +0 -44
  115. data/Pro-4.0-Upgrade.md +0 -35
  116. data/Pro-Changes.md +0 -759
  117. data/Rakefile +0 -9
  118. data/bin/sidekiqctl +0 -20
  119. data/code_of_conduct.md +0 -50
  120. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  121. data/lib/sidekiq/core_ext.rb +0 -1
  122. data/lib/sidekiq/ctl.rb +0 -221
  123. data/lib/sidekiq/exception_handler.rb +0 -29
  124. data/lib/sidekiq/logging.rb +0 -122
  125. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  126. data/lib/sidekiq/util.rb +0 -66
data/lib/sidekiq.rb CHANGED
@@ -1,45 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'sidekiq/version'
4
- fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.2.2." if RUBY_PLATFORM != 'java' && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.2')
3
+ require "sidekiq/version"
4
+ fail "Sidekiq #{Sidekiq::VERSION} does not support Ruby versions below 2.5.0." if RUBY_PLATFORM != "java" && Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5.0")
5
5
 
6
- require 'sidekiq/logging'
7
- require 'sidekiq/client'
8
- require 'sidekiq/worker'
9
- require 'sidekiq/redis_connection'
10
- require 'sidekiq/delay'
6
+ require "sidekiq/logger"
7
+ require "sidekiq/client"
8
+ require "sidekiq/transaction_aware_client"
9
+ require "sidekiq/worker"
10
+ require "sidekiq/job"
11
+ require "sidekiq/redis_connection"
12
+ require "sidekiq/delay"
11
13
 
12
- require 'json'
14
+ require "json"
13
15
 
14
16
  module Sidekiq
15
- NAME = 'Sidekiq'
16
- LICENSE = 'See LICENSE and the LGPL-3.0 for licensing details.'
17
+ NAME = "Sidekiq"
18
+ LICENSE = "See LICENSE and the LGPL-3.0 for licensing details."
17
19
 
18
20
  DEFAULTS = {
19
21
  queues: [],
20
22
  labels: [],
21
23
  concurrency: 10,
22
- require: '.',
24
+ require: ".",
25
+ strict: true,
23
26
  environment: nil,
24
- timeout: 8,
27
+ timeout: 25,
25
28
  poll_interval_average: nil,
26
29
  average_scheduled_poll_interval: 5,
30
+ on_complex_arguments: :warn,
27
31
  error_handlers: [],
28
32
  death_handlers: [],
29
33
  lifecycle_events: {
30
34
  startup: [],
31
35
  quiet: [],
32
36
  shutdown: [],
37
+ # triggers when we fire the first heartbeat on startup OR repairing a network partition
33
38
  heartbeat: [],
39
+ # triggers on EVERY heartbeat call, every 10 seconds
40
+ beat: []
34
41
  },
35
42
  dead_max_jobs: 10_000,
36
43
  dead_timeout_in_seconds: 180 * 24 * 60 * 60, # 6 months
37
- reloader: proc { |&block| block.call },
38
- }
39
-
40
- DEFAULT_WORKER_OPTIONS = {
41
- 'retry' => true,
42
- 'queue' => 'default'
44
+ reloader: proc { |&block| block.call }
43
45
  }
44
46
 
45
47
  FAKE_INFO = {
@@ -54,19 +56,84 @@ module Sidekiq
54
56
  puts "Calm down, yo."
55
57
  end
56
58
 
59
+ # config.concurrency = 5
60
+ def self.concurrency=(val)
61
+ self[:concurrency] = Integer(val)
62
+ end
63
+
64
+ # config.queues = %w( high default low ) # strict
65
+ # config.queues = %w( high,3 default,2 low,1 ) # weighted
66
+ # config.queues = %w( feature1,1 feature2,1 feature3,1 ) # random
67
+ #
68
+ # With weighted priority, queue will be checked first (weight / total) of the time.
69
+ # high will be checked first (3/6) or 50% of the time.
70
+ # I'd recommend setting weights between 1-10. Weights in the hundreds or thousands
71
+ # are ridiculous and unnecessarily expensive. You can get random queue ordering
72
+ # by explicitly setting all weights to 1.
73
+ def self.queues=(val)
74
+ self[:queues] = Array(val).each_with_object([]) do |qstr, memo|
75
+ name, weight = qstr.split(",")
76
+ self[:strict] = false if weight.to_i > 0
77
+ [weight.to_i, 1].max.times do
78
+ memo << name
79
+ end
80
+ end
81
+ end
82
+
83
+ ### Private APIs
84
+ def self.default_error_handler(ex, ctx)
85
+ logger.warn(dump_json(ctx)) unless ctx.empty?
86
+ logger.warn("#{ex.class.name}: #{ex.message}")
87
+ logger.warn(ex.backtrace.join("\n")) unless ex.backtrace.nil?
88
+ end
89
+
90
+ # DEFAULT_ERROR_HANDLER is a constant that allows the default error handler to
91
+ # be referenced. It must be defined here, after the default_error_handler
92
+ # method is defined.
93
+ DEFAULT_ERROR_HANDLER = method(:default_error_handler)
94
+
95
+ @config = DEFAULTS.dup
57
96
  def self.options
58
- @options ||= DEFAULTS.dup
97
+ logger.warn "`config.options[:key] = value` is deprecated, use `config[:key] = value`: #{caller(1..2)}"
98
+ @config
59
99
  end
60
100
 
61
101
  def self.options=(opts)
62
- @options = opts
102
+ logger.warn "config.options = hash` is deprecated, use `config.merge!(hash)`: #{caller(1..2)}"
103
+ @config = opts
104
+ end
105
+
106
+ def self.[](key)
107
+ @config[key]
108
+ end
109
+
110
+ def self.[]=(key, val)
111
+ @config[key] = val
112
+ end
113
+
114
+ def self.merge!(hash)
115
+ @config.merge!(hash)
116
+ end
117
+
118
+ def self.fetch(*args, &block)
119
+ @config.fetch(*args, &block)
120
+ end
121
+
122
+ def self.handle_exception(ex, ctx = {})
123
+ self[:error_handlers].each do |handler|
124
+ handler.call(ex, ctx)
125
+ rescue => ex
126
+ logger.error "!!! ERROR HANDLER THREW AN ERROR !!!"
127
+ logger.error ex
128
+ logger.error ex.backtrace.join("\n") unless ex.backtrace.nil?
129
+ end
63
130
  end
131
+ ###
64
132
 
65
133
  ##
66
134
  # Configuration for Sidekiq server, use like:
67
135
  #
68
136
  # Sidekiq.configure_server do |config|
69
- # config.redis = { :namespace => 'myapp', :size => 25, :url => 'redis://myhost:8877/0' }
70
137
  # config.server_middleware do |chain|
71
138
  # chain.add MyServerHook
72
139
  # end
@@ -79,7 +146,7 @@ module Sidekiq
79
146
  # Configuration for Sidekiq client, use like:
80
147
  #
81
148
  # Sidekiq.configure_client do |config|
82
- # config.redis = { :namespace => 'myapp', :size => 1, :url => 'redis://myhost:8877/0' }
149
+ # config.redis = { size: 1, url: 'redis://myhost:8877/0' }
83
150
  # end
84
151
  def self.configure_client
85
152
  yield self unless server?
@@ -95,10 +162,17 @@ module Sidekiq
95
162
  retryable = true
96
163
  begin
97
164
  yield conn
98
- rescue Redis::CommandError => ex
99
- #2550 Failover can cause the server to become a replica, need
165
+ rescue RedisConnection.adapter::BaseError => ex
166
+ # 2550 Failover can cause the server to become a replica, need
100
167
  # to disconnect and reopen the socket to get back to the primary.
101
- (conn.disconnect!; retryable = false; retry) if retryable && ex.message =~ /READONLY/
168
+ # 4495 Use the same logic if we have a "Not enough replicas" error from the primary
169
+ # 4985 Use the same logic when a blocking command is force-unblocked
170
+ # The same retry logic is also used in client.rb
171
+ if retryable && ex.message =~ /READONLY|NOREPLICAS|UNBLOCKED/
172
+ conn.disconnect!
173
+ retryable = false
174
+ retry
175
+ end
102
176
  raise
103
177
  end
104
178
  end
@@ -106,36 +180,34 @@ module Sidekiq
106
180
 
107
181
  def self.redis_info
108
182
  redis do |conn|
109
- begin
110
- # admin commands can't go through redis-namespace starting
111
- # in redis-namespace 2.0
112
- if conn.respond_to?(:namespace)
113
- conn.redis.info
114
- else
115
- conn.info
116
- end
117
- rescue Redis::CommandError => ex
118
- #2850 return fake version when INFO command has (probably) been renamed
119
- raise unless ex.message =~ /unknown command/
120
- FAKE_INFO
183
+ # admin commands can't go through redis-namespace starting
184
+ # in redis-namespace 2.0
185
+ if conn.respond_to?(:namespace)
186
+ conn.redis.info
187
+ else
188
+ conn.info
121
189
  end
190
+ rescue RedisConnection.adapter::CommandError => ex
191
+ # 2850 return fake version when INFO command has (probably) been renamed
192
+ raise unless /unknown command/.match?(ex.message)
193
+ FAKE_INFO
122
194
  end
123
195
  end
124
196
 
125
197
  def self.redis_pool
126
- @redis ||= Sidekiq::RedisConnection.create
198
+ @redis ||= RedisConnection.create
127
199
  end
128
200
 
129
201
  def self.redis=(hash)
130
202
  @redis = if hash.is_a?(ConnectionPool)
131
203
  hash
132
204
  else
133
- Sidekiq::RedisConnection.create(hash)
205
+ RedisConnection.create(hash)
134
206
  end
135
207
  end
136
208
 
137
209
  def self.client_middleware
138
- @client_chain ||= Middleware::Chain.new
210
+ @client_chain ||= Middleware::Chain.new(self)
139
211
  yield @client_chain if block_given?
140
212
  @client_chain
141
213
  end
@@ -147,21 +219,23 @@ module Sidekiq
147
219
  end
148
220
 
149
221
  def self.default_server_middleware
150
- Middleware::Chain.new
222
+ Middleware::Chain.new(self)
151
223
  end
152
224
 
153
- def self.default_worker_options=(hash)
154
- # stringify
155
- @default_worker_options = default_worker_options.merge(Hash[hash.map{|k, v| [k.to_s, v]}])
225
+ def self.default_worker_options=(hash) # deprecated
226
+ @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
156
227
  end
157
- def self.default_worker_options
158
- defined?(@default_worker_options) ? @default_worker_options : DEFAULT_WORKER_OPTIONS
228
+
229
+ def self.default_job_options=(hash)
230
+ @default_job_options = default_job_options.merge(hash.transform_keys(&:to_s))
231
+ end
232
+
233
+ def self.default_worker_options # deprecated
234
+ @default_job_options ||= {"retry" => true, "queue" => "default"}
159
235
  end
160
236
 
161
- def self.default_retries_exhausted=(prok)
162
- logger.info { "default_retries_exhausted is deprecated, please use `config.death_handlers << -> {|job, ex| }`" }
163
- return nil unless prok
164
- death_handlers << prok
237
+ def self.default_job_options
238
+ @default_job_options ||= {"retry" => true, "queue" => "default"}
165
239
  end
166
240
 
167
241
  ##
@@ -174,21 +248,51 @@ module Sidekiq
174
248
  # end
175
249
  # end
176
250
  def self.death_handlers
177
- options[:death_handlers]
251
+ self[:death_handlers]
178
252
  end
179
253
 
180
254
  def self.load_json(string)
181
255
  JSON.parse(string)
182
256
  end
257
+
183
258
  def self.dump_json(object)
184
259
  JSON.generate(object)
185
260
  end
186
261
 
262
+ def self.log_formatter
263
+ @log_formatter ||= if ENV["DYNO"]
264
+ Sidekiq::Logger::Formatters::WithoutTimestamp.new
265
+ else
266
+ Sidekiq::Logger::Formatters::Pretty.new
267
+ end
268
+ end
269
+
270
+ def self.log_formatter=(log_formatter)
271
+ @log_formatter = log_formatter
272
+ logger.formatter = log_formatter
273
+ end
274
+
187
275
  def self.logger
188
- Sidekiq::Logging.logger
276
+ @logger ||= Sidekiq::Logger.new($stdout, level: :info)
189
277
  end
190
- def self.logger=(log)
191
- Sidekiq::Logging.logger = log
278
+
279
+ def self.logger=(logger)
280
+ if logger.nil?
281
+ self.logger.level = Logger::FATAL
282
+ return self.logger
283
+ end
284
+
285
+ logger.extend(Sidekiq::LoggingUtils)
286
+
287
+ @logger = logger
288
+ end
289
+
290
+ def self.pro?
291
+ defined?(Sidekiq::Pro)
292
+ end
293
+
294
+ def self.ent?
295
+ defined?(Sidekiq::Enterprise)
192
296
  end
193
297
 
194
298
  # How frequently Redis should be checked by a random Sidekiq process for
@@ -197,7 +301,7 @@ module Sidekiq
197
301
  #
198
302
  # See sidekiq/scheduled.rb for an in-depth explanation of this value
199
303
  def self.average_scheduled_poll_interval=(interval)
200
- self.options[:average_scheduled_poll_interval] = interval
304
+ self[:average_scheduled_poll_interval] = interval
201
305
  end
202
306
 
203
307
  # Register a proc to handle any error which occurs within the Sidekiq process.
@@ -208,7 +312,7 @@ module Sidekiq
208
312
  #
209
313
  # The default error handler logs errors to Sidekiq.logger.
210
314
  def self.error_handlers
211
- self.options[:error_handlers]
315
+ self[:error_handlers]
212
316
  end
213
317
 
214
318
  # Register a block to run at a point in the Sidekiq lifecycle.
@@ -221,17 +325,21 @@ module Sidekiq
221
325
  # end
222
326
  def self.on(event, &block)
223
327
  raise ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
224
- raise ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
225
- options[:lifecycle_events][event] << block
328
+ raise ArgumentError, "Invalid event name: #{event}" unless self[:lifecycle_events].key?(event)
329
+ self[:lifecycle_events][event] << block
330
+ end
331
+
332
+ def self.strict_args!(mode = :raise)
333
+ self[:on_complex_arguments] = mode
226
334
  end
227
335
 
228
- # We are shutting down Sidekiq but what about workers that
336
+ # We are shutting down Sidekiq but what about threads that
229
337
  # are working on some long job? This error is
230
- # raised in workers that have not finished within the hard
338
+ # raised in jobs that have not finished within the hard
231
339
  # timeout limit. This is needed to rollback db transactions,
232
340
  # otherwise Ruby's Thread#kill will commit. See #377.
233
- # DO NOT RESCUE THIS ERROR IN YOUR WORKERS
341
+ # DO NOT RESCUE THIS ERROR IN YOUR JOBS
234
342
  class Shutdown < Interrupt; end
235
343
  end
236
344
 
237
- require 'sidekiq/rails' if defined?(::Rails::Engine)
345
+ require "sidekiq/rails" if defined?(::Rails::Engine)
data/sidekiq.gemspec CHANGED
@@ -1,21 +1,28 @@
1
- require_relative 'lib/sidekiq/version'
1
+ require_relative "lib/sidekiq/version"
2
2
 
3
3
  Gem::Specification.new do |gem|
4
- gem.authors = ["Mike Perham"]
5
- gem.email = ["mperham@gmail.com"]
6
- gem.summary = "Simple, efficient background processing for Ruby"
7
- gem.description = "Simple, efficient background processing for Ruby."
8
- gem.homepage = "http://sidekiq.org"
9
- gem.license = "LGPL-3.0"
4
+ gem.authors = ["Mike Perham"]
5
+ gem.email = ["mperham@gmail.com"]
6
+ gem.summary = "Simple, efficient background processing for Ruby"
7
+ gem.description = "Simple, efficient background processing for Ruby."
8
+ gem.homepage = "https://sidekiq.org"
9
+ gem.license = "LGPL-3.0"
10
10
 
11
- gem.executables = ['sidekiq', 'sidekiqctl']
12
- gem.files = `git ls-files | grep -Ev '^(test|myapp|examples)'`.split("\n")
13
- gem.name = "sidekiq"
14
- gem.version = Sidekiq::VERSION
15
- gem.required_ruby_version = ">= 2.2.2"
11
+ gem.executables = ["sidekiq", "sidekiqmon"]
12
+ gem.files = ["sidekiq.gemspec", "README.md", "Changes.md", "LICENSE"] + `git ls-files | grep -E '^(bin|lib|web)'`.split("\n")
13
+ gem.name = "sidekiq"
14
+ gem.version = Sidekiq::VERSION
15
+ gem.required_ruby_version = ">= 2.5.0"
16
16
 
17
- gem.add_dependency "redis", "~> 4.5", "< 4.6.0"
18
- gem.add_dependency 'connection_pool', '~> 2.2', '>= 2.2.2'
19
- gem.add_dependency 'rack', '~> 2.0'
20
- gem.add_dependency 'rack-protection', '>= 1.5.0'
17
+ gem.metadata = {
18
+ "homepage_uri" => "https://sidekiq.org",
19
+ "bug_tracker_uri" => "https://github.com/mperham/sidekiq/issues",
20
+ "documentation_uri" => "https://github.com/mperham/sidekiq/wiki",
21
+ "changelog_uri" => "https://github.com/mperham/sidekiq/blob/main/Changes.md",
22
+ "source_code_uri" => "https://github.com/mperham/sidekiq"
23
+ }
24
+
25
+ gem.add_dependency "redis", ["<5", ">= 4.5.0"]
26
+ gem.add_dependency "connection_pool", ["<3", ">= 2.2.5"]
27
+ gem.add_dependency "rack", "~> 2.0"
21
28
  end