sidekiq-ultimate 0.0.1.alpha.19 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sidekiq"
3
4
  require "sidekiq/throttled"
4
5
 
5
6
  require "sidekiq/ultimate/expirable_set"
6
7
  require "sidekiq/ultimate/queue_name"
7
8
  require "sidekiq/ultimate/resurrector"
8
9
  require "sidekiq/ultimate/unit_of_work"
10
+ require "sidekiq/ultimate/empty_queues"
11
+ require "sidekiq/ultimate/configuration"
9
12
 
10
13
  module Sidekiq
11
14
  module Ultimate
@@ -14,17 +17,11 @@ module Sidekiq
14
17
  # Delay between fetch retries in case of no job received.
15
18
  TIMEOUT = 2
16
19
 
17
- # Delay between queue poll attempts if last poll returned no jobs for it.
18
- QUEUE_TIMEOUT = 5
19
-
20
- # Delay between queue poll attempts if it's last job was throttled.
21
- THROTTLE_TIMEOUT = 15
22
-
23
20
  def initialize(options)
24
- @exhausted = ExpirableSet.new
25
-
21
+ @exhausted_by_throttling = ExpirableSet.new
22
+ @empty_queues = Sidekiq::Ultimate::EmptyQueues.instance
26
23
  @strict = options[:strict] ? true : false
27
- @queues = options[:queues].map { |name| QueueName.new(name) }
24
+ @queues = options[:queues]
28
25
 
29
26
  @queues.uniq! if @strict
30
27
 
@@ -39,8 +36,9 @@ module Sidekiq
39
36
  if work&.throttled?
40
37
  work.requeue_throttled
41
38
 
42
- queue = QueueName.new(work.queue_name)
43
- @exhausted.add(queue, :ttl => THROTTLE_TIMEOUT)
39
+ @exhausted_by_throttling.add(
40
+ work.queue_name, :ttl => Sidekiq::Ultimate::Configuration.instance.throttled_fetch_timeout_sec
41
+ )
44
42
 
45
43
  return nil
46
44
  end
@@ -48,24 +46,26 @@ module Sidekiq
48
46
  work
49
47
  end
50
48
 
51
- def self.bulk_requeue(units, _options)
49
+ # TODO: Requeue in batch or at least using pipeline
50
+ def bulk_requeue(units, _options)
52
51
  units.each(&:requeue)
53
52
  end
54
53
 
55
54
  def self.setup!
56
- Sidekiq.options[:fetch] = self
55
+ fetcher = new(Sidekiq)
56
+
57
+ Sidekiq[:fetch] = fetcher
57
58
  Resurrector.setup!
59
+ EmptyQueues.setup!
58
60
  end
59
61
 
60
62
  private
61
63
 
62
64
  def retrieve
63
65
  Sidekiq.redis do |redis|
64
- queues.each do |queue|
66
+ queues_objects.each do |queue|
65
67
  job = redis.rpoplpush(queue.pending, queue.inproc)
66
68
  return UnitOfWork.new(queue, job) if job
67
-
68
- @exhausted.add(queue, :ttl => QUEUE_TIMEOUT)
69
69
  end
70
70
  end
71
71
 
@@ -73,19 +73,19 @@ module Sidekiq
73
73
  nil
74
74
  end
75
75
 
76
- def queues
77
- queues = (@strict ? @queues : @queues.shuffle.uniq) - @exhausted.to_a
76
+ def queues_objects
77
+ queues = (@strict ? @queues : @queues.shuffle.uniq) - @exhausted_by_throttling.to_a - @empty_queues.queues
78
78
 
79
79
  # Avoid calling heavier `paused_queue` if there's nothing to filter out
80
- return queues if queues.empty?
80
+ return [] if queues.empty?
81
81
 
82
- queues - paused_queues
82
+ (queues - paused_queues).map { |name| QueueName.new(name) }
83
83
  end
84
84
 
85
85
  def paused_queues
86
86
  return @paused_queues if Time.now.to_i < @paused_queues_expires_at
87
87
 
88
- @paused_queues.replace(Sidekiq::Throttled::QueuesPauser.instance.paused_queues.map { |q| QueueName[q] })
88
+ @paused_queues = Sidekiq::Throttled::QueuesPauser.instance.paused_queues
89
89
  @paused_queues_expires_at = Time.now.to_i + 60
90
90
 
91
91
  @paused_queues
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Ultimate
5
+ # Util class to add a jitter to the interval
6
+ class IntervalWithJitter
7
+ RANDOM_OFFSET_RATIO = 0.1
8
+
9
+ class << self
10
+ # Returns execution interval with jitter.
11
+ # Jitter is +- RANDOM_OFFSET_RATIO from the original value.
12
+ def call(interval)
13
+ jitter_factor = 1 + rand(-RANDOM_OFFSET_RATIO..RANDOM_OFFSET_RATIO)
14
+ jitter_factor * interval
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sidekiq/util"
3
+ require "sidekiq/component"
4
4
 
5
5
  module Sidekiq
6
6
  module Ultimate
@@ -10,11 +10,11 @@ module Sidekiq
10
10
  class QueueName
11
11
  # Regexp used to normalize (possibly) expanded queue name, e.g. the one
12
12
  # that is returned upon redis BRPOP
13
- QUEUE_PREFIX_RE = %r{.*queue:}
13
+ QUEUE_PREFIX_RE = %r{.*queue:}.freeze
14
14
  private_constant :QUEUE_PREFIX_RE
15
15
 
16
16
  # Internal helper context.
17
- Helper = Module.new { extend Sidekiq::Util }
17
+ Helper = Module.new { extend Sidekiq::Component }
18
18
  private_constant :Helper
19
19
 
20
20
  # Original stringified queue name.
@@ -29,8 +29,7 @@ module Sidekiq
29
29
 
30
30
  # Create a new QueueName instance.
31
31
  #
32
- # @param normalized [#to_s] Normalized (without any namespaces or `queue:`
33
- # prefixes) queue name.
32
+ # @param normalized [#to_s] Normalized (without `queue:` prefixes) queue name.
34
33
  # @param identity [#to_s] Sidekiq process identity.
35
34
  def initialize(normalized, identity: Helper.identity)
36
35
  @normalized = -normalized.to_s
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module Ultimate
5
+ module Resurrector
6
+ module CommonConstants
7
+ MAIN_KEY = "ultimate:resurrector"
8
+
9
+ RESURRECTOR_INTERVAL = 60
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/ultimate/resurrector/common_constants"
4
+
5
+ module Sidekiq
6
+ module Ultimate
7
+ module Resurrector
8
+ # Allows to get the count of times the job was resurrected
9
+ module Count
10
+ class << self
11
+ # @param job_id [String] job id
12
+ # @return [Integer] count of times the job was resurrected
13
+ def read(job_id:)
14
+ Sidekiq.redis do |redis|
15
+ redis.get("#{CommonConstants::MAIN_KEY}:counter:jid:#{job_id}").to_i
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redlock"
4
+ require "sidekiq/ultimate/resurrector/common_constants"
5
+
6
+ module Sidekiq
7
+ module Ultimate
8
+ module Resurrector
9
+ # Ensures exclusive access to resurrection process
10
+ class Lock
11
+ LOCK_KEY = "#{CommonConstants::MAIN_KEY}:lock"
12
+ private_constant :LOCK_KEY
13
+
14
+ LAST_RUN_KEY = "#{CommonConstants::MAIN_KEY}:last_run"
15
+ private_constant :LAST_RUN_KEY
16
+
17
+ LOCK_TTL = 30_000 # ms
18
+ private_constant :LOCK_TTL
19
+
20
+ class << self
21
+ def acquire
22
+ Sidekiq.redis do |redis|
23
+ break if resurrected_recently?(redis) # Cheap check since lock will not be free most of the time
24
+
25
+ Redlock::Client.new([redis], :retry_count => 0).lock(LOCK_KEY, LOCK_TTL) do |locked|
26
+ break unless locked
27
+ break if resurrected_recently?(redis)
28
+
29
+ yield
30
+
31
+ redis.set(LAST_RUN_KEY, redis.time.first)
32
+ end
33
+ end
34
+ end
35
+
36
+ def resurrected_recently?(redis)
37
+ results = redis.pipelined { |pipeline| [pipeline.time, pipeline.get(LAST_RUN_KEY)] }
38
+ distance = results[0][0] - results[1].to_i
39
+
40
+ distance < CommonConstants::RESURRECTOR_INTERVAL
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ local resurrected_jobs = 0
2
+
3
+ while true do
4
+ local job_data = redis.call("LPOP", KEYS[1])
5
+
6
+ if job_data then
7
+ redis.call("RPUSH", KEYS[2], job_data)
8
+
9
+ resurrected_jobs = resurrected_jobs + 1
10
+
11
+ local _, jid_position = string.find(job_data, "\"jid\"")
12
+ jid_position = jid_position + 3
13
+
14
+ local jid = job_data:sub(jid_position, jid_position + 23)
15
+ local jid_key = KEYS[3] .. ':counter:jid:' .. jid
16
+
17
+ redis.call("INCR", jid_key)
18
+ redis.call("EXPIRE", jid_key, 86400)
19
+ else
20
+ return resurrected_jobs
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis_prescription"
4
+ require "sidekiq/ultimate/configuration"
5
+ require "sidekiq/ultimate/resurrector/common_constants"
6
+
7
+ module Sidekiq
8
+ module Ultimate
9
+ module Resurrector
10
+ # Lost jobs checker and resurrector
11
+ class ResurrectionScript
12
+ RESURRECT = RedisPrescription.new(File.read("#{__dir__}/lua_scripts/resurrect.lua"))
13
+ private_constant :RESURRECT
14
+
15
+ RESURRECT_WITH_COUNTER = RedisPrescription.new(File.read("#{__dir__}/lua_scripts/resurrect_with_counter.lua"))
16
+ private_constant :RESURRECT_WITH_COUNTER
17
+
18
+ def self.call(*args)
19
+ new.call(*args)
20
+ end
21
+
22
+ def call(redis, keys:)
23
+ # redis-namespace can only namespace arguments of the lua script, so we need to pass the main key
24
+ keys += [CommonConstants::MAIN_KEY] if enable_resurrection_counter
25
+ script.call(redis, :keys => keys)
26
+ end
27
+
28
+ private
29
+
30
+ def script
31
+ enable_resurrection_counter ? RESURRECT_WITH_COUNTER : RESURRECT
32
+ end
33
+
34
+ def enable_resurrection_counter
35
+ return @enable_resurrection_counter if defined?(@enable_resurrection_counter)
36
+
37
+ @enable_resurrection_counter =
38
+ if enable_resurrection_counter_setting.respond_to?(:call)
39
+ enable_resurrection_counter_setting.call
40
+ else
41
+ enable_resurrection_counter_setting
42
+ end
43
+ end
44
+
45
+ def enable_resurrection_counter_setting
46
+ Sidekiq::Ultimate::Configuration.instance.enable_resurrection_counter
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,41 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redis/lockers"
4
- require "redis/prescription"
3
+ require "redis_prescription"
4
+ require "concurrent/timer_task"
5
5
 
6
+ require "sidekiq/component"
6
7
  require "sidekiq/ultimate/queue_name"
8
+ require "sidekiq/ultimate/resurrector/lock"
9
+ require "sidekiq/ultimate/resurrector/common_constants"
10
+ require "sidekiq/ultimate/resurrector/resurrection_script"
11
+ require "sidekiq/ultimate/configuration"
12
+ require "sidekiq/ultimate/interval_with_jitter"
7
13
 
8
14
  module Sidekiq
9
15
  module Ultimate
10
- # Lost jobs resurrector.
16
+ # Lost jobs checker and resurrector
11
17
  module Resurrector
12
- RESURRECT = Redis::Prescription.read \
13
- "#{__dir__}/resurrector/resurrect.lua"
14
- private_constant :RESURRECT
15
-
16
- SAFECLEAN = Redis::Prescription.read \
17
- "#{__dir__}/resurrector/safeclean.lua"
18
+ SAFECLEAN = RedisPrescription.new(File.read("#{__dir__}/resurrector/lua_scripts/safeclean.lua"))
18
19
  private_constant :SAFECLEAN
19
20
 
20
- MAIN_KEY = "ultimate:resurrector"
21
- private_constant :MAIN_KEY
22
-
23
- LOCK_KEY = "#{MAIN_KEY}:lock"
24
- private_constant :LOCK_KEY
21
+ DEFIBRILLATE_INTERVAL = 5
22
+ private_constant :DEFIBRILLATE_INTERVAL
25
23
 
26
- LAST_RUN_KEY = "#{MAIN_KEY}:last_run"
27
- private_constant :LAST_RUN_KEY
24
+ ResurrectorTimerTask = Class.new(Concurrent::TimerTask)
25
+ HeartbeatTimerTask = Class.new(Concurrent::TimerTask)
28
26
 
29
27
  class << self
30
28
  def setup!
31
- @identity = Object.new.tap { |o| o.extend Sidekiq::Util }.identity
32
-
33
- register_aed!
34
- call_cthulhu!
29
+ register_process_heartbeat
30
+ register_resurrector
35
31
  end
36
32
 
33
+ # go over all sidekiq processes (identities) that were shut down recently, get all their queues and
34
+ # try to resurrect them
37
35
  def resurrect!
38
- lock do
36
+ Sidekiq::Ultimate::Resurrector::Lock.acquire do
39
37
  casualties.each do |identity|
40
38
  log(:debug) { "Resurrecting #{identity}" }
41
39
 
@@ -48,102 +46,97 @@ module Sidekiq
48
46
  raise
49
47
  end
50
48
 
49
+ def current_process_identity
50
+ @current_process_identity ||= Object.new.tap { |o| o.extend Sidekiq::Component }.identity
51
+ end
52
+
51
53
  private
52
54
 
53
- def call_cthulhu!
54
- cthulhu = nil
55
+ def register_resurrector
56
+ resurrector_timer_task = nil
55
57
 
56
58
  Sidekiq.on(:startup) do
57
- cthulhu&.shutdown
59
+ resurrector_timer_task&.shutdown
58
60
 
59
- cthulhu = Concurrent::TimerTask.execute({
61
+ resurrector_timer_task = ResurrectorTimerTask.new({
60
62
  :run_now => true,
61
- :execution_interval => 60
63
+ :execution_interval => Sidekiq::Ultimate::IntervalWithJitter.call(CommonConstants::RESURRECTOR_INTERVAL)
62
64
  }) { resurrect! }
65
+ resurrector_timer_task.execute
63
66
  end
64
67
 
65
- Sidekiq.on(:shutdown) { cthulhu&.shutdown }
68
+ Sidekiq.on(:shutdown) { resurrector_timer_task&.shutdown }
66
69
  end
67
70
 
68
- def register_aed!
69
- aed = nil
71
+ def register_process_heartbeat
72
+ heartbeat_timer_task = nil
70
73
 
71
74
  Sidekiq.on(:heartbeat) do
72
- aed&.shutdown
75
+ heartbeat_timer_task&.shutdown
73
76
 
74
- aed = Concurrent::TimerTask.execute({
77
+ heartbeat_timer_task = HeartbeatTimerTask.new({
75
78
  :run_now => true,
76
- :execution_interval => 5
77
- }) { defibrillate! }
79
+ :execution_interval => Sidekiq::Ultimate::IntervalWithJitter.call(DEFIBRILLATE_INTERVAL)
80
+ }) { save_watched_queues }
81
+ heartbeat_timer_task.execute
78
82
  end
79
83
 
80
- Sidekiq.on(:shutdown) { aed&.shutdown }
84
+ Sidekiq.on(:shutdown) { heartbeat_timer_task&.shutdown }
81
85
  end
82
86
 
83
- def defibrillate!
87
+ # put current list of queues into resurrection candidates
88
+ def save_watched_queues
84
89
  Sidekiq.redis do |redis|
85
90
  log(:debug) { "Defibrillating" }
86
91
 
87
- queues = JSON.dump(Sidekiq.options[:queues].uniq)
88
- redis.hset(MAIN_KEY, @identity, queues)
89
- end
90
- end
91
-
92
- def lock
93
- Sidekiq.redis do |redis|
94
- Redis::Lockers.acquire(redis, LOCK_KEY, :ttl => 30_000) do
95
- results = redis.pipelined { |r| [r.time, r.get(LAST_RUN_KEY)] }
96
- distance = results[0][0] - results[1].to_i
97
-
98
- break unless 60 < distance
99
-
100
- yield
101
-
102
- redis.set(LAST_RUN_KEY, redis.time.first)
103
- end
92
+ queues = JSON.dump(Sidekiq[:queues].uniq)
93
+ redis.hset(CommonConstants::MAIN_KEY, current_process_identity, queues)
104
94
  end
105
95
  end
106
96
 
97
+ # list of processes that disappeared after latest #save_watched_queues
107
98
  def casualties
108
99
  Sidekiq.redis do |redis|
109
- casualties = []
110
- identities = redis.hkeys(MAIN_KEY)
100
+ sidekiq_processes = redis.hkeys(CommonConstants::MAIN_KEY)
111
101
 
112
- redis.pipelined { identities.each { |k| redis.exists k } }.
113
- each_with_index { |v, i| casualties << identities[i] unless v }
102
+ sidekiq_processes_alive = redis.pipelined do |pipeline|
103
+ sidekiq_processes.each do |process|
104
+ pipeline.exists?(process)
105
+ end
106
+ end
114
107
 
115
- casualties
108
+ sidekiq_processes.zip(sidekiq_processes_alive).reject { |(_, alive)| alive }.map(&:first)
116
109
  end
117
110
  end
118
111
 
112
+ # Get list of genuine sidekiq queues names for a given identity (sidekiq process id)
119
113
  def queues_of(identity)
120
114
  Sidekiq.redis do |redis|
121
- queues = redis.hget(MAIN_KEY, identity)
115
+ queues = redis.hget(CommonConstants::MAIN_KEY, identity)
122
116
 
123
117
  return [] unless queues
124
118
 
125
- JSON.parse(queues).map do |q|
126
- QueueName.new(q, :identity => identity)
127
- end
119
+ JSON.parse(queues).map { |q| QueueName.new(q, :identity => identity) }
128
120
  end
129
121
  end
130
122
 
123
+ # Move jobs from inproc to pending
131
124
  def resurrect(queue)
132
125
  Sidekiq.redis do |redis|
133
- result = RESURRECT.eval(redis, {
134
- :keys => [queue.inproc, queue.pending]
135
- })
126
+ resurrected_jobs_count = ResurrectionScript.call(redis, :keys => [queue.inproc, queue.pending])
136
127
 
137
- if result.positive?
138
- log(:info) { "Resurrected #{result} jobs from #{queue.inproc}" }
128
+ if resurrected_jobs_count.positive?
129
+ log(:info) { "Resurrected #{resurrected_jobs_count} jobs from #{queue.inproc}" }
130
+ Sidekiq::Ultimate::Configuration.instance.on_resurrection&.call(queue.to_s, resurrected_jobs_count)
139
131
  end
140
132
  end
141
133
  end
142
134
 
135
+ # Delete empty inproc queues and clean up identity key from resurrection candidates (CommonConstants::MAIN_KEY)
143
136
  def cleanup(identity, inprocs)
144
137
  Sidekiq.redis do |redis|
145
- result = SAFECLEAN.eval(redis, {
146
- :keys => [MAIN_KEY, *inprocs],
138
+ result = SAFECLEAN.call(redis, {
139
+ :keys => [CommonConstants::MAIN_KEY, *inprocs],
147
140
  :argv => [identity]
148
141
  })
149
142
 
@@ -153,7 +146,7 @@ module Sidekiq
153
146
 
154
147
  def log(level)
155
148
  Sidekiq.logger.public_send(level) do
156
- "[#{self}] @#{@identity} #{yield}"
149
+ "[#{self}] @#{current_process_identity} #{yield}"
157
150
  end
158
151
  end
159
152
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "redis/prescription"
3
+ require "redis_prescription"
4
4
 
5
5
  require "sidekiq/throttled"
6
6
 
@@ -10,8 +10,7 @@ module Sidekiq
10
10
  #
11
11
  # @private
12
12
  class UnitOfWork
13
- REQUEUE = Redis::Prescription.read \
14
- "#{__dir__}/unit_of_work/requeue.lua"
13
+ REQUEUE = RedisPrescription.new(File.read("#{__dir__}/unit_of_work/requeue.lua"))
15
14
  private_constant :REQUEUE
16
15
 
17
16
  # JSON payload
@@ -90,12 +89,14 @@ module Sidekiq
90
89
 
91
90
  private
92
91
 
92
+ # If the jobs was in the inproc queue, then delete it from there and
93
+ # push the job back to the queue using `command`.
93
94
  def __requeue__(command)
94
95
  @mutex.synchronize do
95
96
  return if @requeued || @acked
96
97
 
97
98
  Sidekiq.redis do |redis|
98
- REQUEUE.eval(redis, {
99
+ REQUEUE.call(redis, {
99
100
  :keys => [@queue.pending, @queue.inproc],
100
101
  :argv => [command, @job]
101
102
  })
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module Ultimate
5
- # Gem version.
6
- VERSION = "0.0.1.alpha.19"
5
+ VERSION = "2.0.0"
7
6
  end
8
7
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "sidekiq/throttled"
4
4
 
5
- require "sidekiq/ultimate/version"
5
+ require "sidekiq/ultimate/configuration"
6
+ require "sidekiq/ultimate/resurrector/count"
6
7
 
7
8
  module Sidekiq
8
9
  # Sidekiq ultimate experience.
@@ -10,10 +11,18 @@ module Sidekiq
10
11
  class << self
11
12
  # Sets up reliable throttled fetch and friends.
12
13
  # @return [void]
13
- def setup!
14
+ def setup!(&configuration_block)
15
+ configuration_block&.call(Sidekiq::Ultimate::Configuration.instance)
16
+
14
17
  Sidekiq::Throttled::Communicator.instance.setup!
15
18
  Sidekiq::Throttled::QueuesPauser.instance.setup!
16
19
 
20
+ sidekiq_configure_server
21
+ end
22
+
23
+ private
24
+
25
+ def sidekiq_configure_server
17
26
  Sidekiq.configure_server do |config|
18
27
  require "sidekiq/ultimate/fetch"
19
28
  Sidekiq::Ultimate::Fetch.setup!
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  require "sidekiq/ultimate/version"
7
7
 
8
- Gem::Specification.new do |spec|
8
+ Gem::Specification.new do |spec| # rubocop:disable Gemspec/RequireMFA
9
9
  spec.name = "sidekiq-ultimate"
10
10
  spec.version = Sidekiq::Ultimate::VERSION
11
11
  spec.authors = ["Alexey Zapparov"]
@@ -23,15 +23,13 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ["lib"]
24
24
 
25
25
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
26
- spec.add_runtime_dependency "redis-lockers", "~> 1.1"
27
- spec.add_runtime_dependency "redis-prescription", "~> 1.0"
28
- spec.add_runtime_dependency "sidekiq", "~> 5.0"
29
-
30
- # temporary couple this with sidekiq-throttled until it will be merged into
31
- # this gem instead.
32
- spec.add_runtime_dependency "sidekiq-throttled", "~> 0.8"
26
+ spec.add_runtime_dependency "redis", "~> 4.8"
27
+ spec.add_runtime_dependency "redis-prescription", "~> 2.6"
28
+ spec.add_runtime_dependency "redlock", "~> 1.3"
29
+ spec.add_runtime_dependency "sidekiq", "~> 6.5.0"
30
+ spec.add_runtime_dependency "sidekiq-throttled", "~> 0.18.0"
33
31
 
34
32
  spec.add_development_dependency "bundler", "~> 2.0"
35
33
 
36
- spec.required_ruby_version = "~> 2.3"
34
+ spec.required_ruby_version = "~> 2.7"
37
35
  end