sidekiq 5.2.10 → 6.5.6

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +391 -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 +504 -307
  13. data/lib/sidekiq/cli.rb +190 -206
  14. data/lib/sidekiq/client.rb +77 -81
  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 +20 -16
  39. data/lib/sidekiq/processor.rb +104 -97
  40. data/lib/sidekiq/rails.rb +47 -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 +64 -35
  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 +100 -77
  53. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  54. data/lib/sidekiq/web/helpers.rb +134 -94
  55. data/lib/sidekiq/web/router.rb +23 -19
  56. data/lib/sidekiq/web.rb +65 -105
  57. data/lib/sidekiq/worker.rb +253 -106
  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 +112 -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 +7 -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/views/_footer.erb +1 -1
  83. data/web/views/_job_info.erb +3 -2
  84. data/web/views/_nav.erb +1 -1
  85. data/web/views/_poll_link.erb +2 -5
  86. data/web/views/_summary.erb +7 -7
  87. data/web/views/busy.erb +56 -22
  88. data/web/views/dashboard.erb +23 -14
  89. data/web/views/dead.erb +3 -3
  90. data/web/views/layout.erb +3 -1
  91. data/web/views/metrics.erb +69 -0
  92. data/web/views/metrics_for_job.erb +87 -0
  93. data/web/views/morgue.erb +9 -6
  94. data/web/views/queue.erb +23 -10
  95. data/web/views/queues.erb +10 -2
  96. data/web/views/retries.erb +11 -8
  97. data/web/views/retry.erb +3 -3
  98. data/web/views/scheduled.erb +5 -2
  99. metadata +53 -64
  100. data/.circleci/config.yml +0 -61
  101. data/.github/contributing.md +0 -32
  102. data/.github/issue_template.md +0 -11
  103. data/.gitignore +0 -15
  104. data/.travis.yml +0 -11
  105. data/3.0-Upgrade.md +0 -70
  106. data/4.0-Upgrade.md +0 -53
  107. data/5.0-Upgrade.md +0 -56
  108. data/COMM-LICENSE +0 -97
  109. data/Ent-Changes.md +0 -238
  110. data/Gemfile +0 -19
  111. data/Pro-2.0-Upgrade.md +0 -138
  112. data/Pro-3.0-Upgrade.md +0 -44
  113. data/Pro-4.0-Upgrade.md +0 -35
  114. data/Pro-Changes.md +0 -759
  115. data/Rakefile +0 -9
  116. data/bin/sidekiqctl +0 -20
  117. data/code_of_conduct.md +0 -50
  118. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  119. data/lib/sidekiq/core_ext.rb +0 -1
  120. data/lib/sidekiq/ctl.rb +0 -221
  121. data/lib/sidekiq/exception_handler.rb +0 -29
  122. data/lib/sidekiq/logging.rb +0 -122
  123. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  124. 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", ">= 2.2.5"
27
+ gem.add_dependency "rack", "~> 2.0"
21
28
  end