sidekiq-ultimate 0.0.1.alpha.19 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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