redlock 1.0.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09e97229d3f4d001a612e97e6c76a7855a7186ec612afaee0d6e7b1dc5fd2cfc'
4
- data.tar.gz: ad13c6b86a28eaba8359729b9f857c147610dbac0bb272613e2950b6c5adbee8
3
+ metadata.gz: 1870dc8b8577a8e8545fd952fd6c2f1ff3ac1f336bff5f632b3b89eb92eb0069
4
+ data.tar.gz: b5153369a5cd331a4bbc0884efd657692da4b824e0cbe0dd8ed78fdd407d7047
5
5
  SHA512:
6
- metadata.gz: cb58e84074840f2a08ce2b6d53385ec86faf3a94a1579b4ca3477278a7ba95bbd3ffaeff5adaadc4d3515c84f97659a45177393c1e6ebda13ef9458dd28eae06
7
- data.tar.gz: 6a5a52c8d48cb09f64bb071dccda4518bdc0e07ddea596ac152a6ed52ff6fdf473de6565ce68ecbf4077008acc289c32eeda41d4617ec9370d5e2cabada86f2d
6
+ metadata.gz: 747aa1155827f513a0612e6610dd754143c3fb65c50aaeca87506830d91c83ec40a89f7fcfb81d22395ea2cc16048cc99136894f0b6a8adb7455d2adae60c244
7
+ data.tar.gz: b033303f287bdbeca822c6b3f0ad829531c5740ad0716f6a6cdeb75370ea9e3874838b761f65ac242b07a07af0b1c0ab955e288059f0953aed3aeb686773a6cc
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  *.gem
2
2
  coverage/
3
3
  .bundle/
4
+ dump.rdb
data/.travis.yml CHANGED
@@ -1,8 +1,23 @@
1
1
  language: ruby
2
- services:
3
- - redis-server
2
+ cache: bundler
3
+ sudo: false
4
+
4
5
  rvm:
5
- - "2.2.2"
6
+ - 2.4.9
7
+ - 2.5.7
8
+ - 2.6.5
9
+ - 2.7.0
10
+ - ruby-head
11
+
12
+ before_install:
13
+ - yes | gem update --system
14
+ - gem install bundler -v "~> 2.0"
15
+
6
16
  script: bundle exec rspec spec
7
- sudo: false
8
- cache: bundler
17
+
18
+ jobs:
19
+ allow_failures:
20
+ - rvm: ruby-head
21
+
22
+ services:
23
+ - redis-server
data/Gemfile.lock CHANGED
@@ -1,54 +1,59 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redlock (1.0.0)
4
+ redlock (1.2.2)
5
5
  redis (>= 3.0.0, < 5.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- coveralls (0.8.22)
10
+ connection_pool (2.2.5)
11
+ coveralls (0.8.23)
11
12
  json (>= 1.8, < 3)
12
13
  simplecov (~> 0.16.1)
13
14
  term-ansicolor (~> 1.3)
14
- thor (~> 0.19.4)
15
+ thor (>= 0.19.4, < 2.0)
15
16
  tins (~> 1.6)
16
- diff-lcs (1.3)
17
- docile (1.3.1)
18
- json (2.1.0)
19
- rake (11.3.0)
20
- redis (4.0.3)
21
- rspec (3.5.0)
22
- rspec-core (~> 3.5.0)
23
- rspec-expectations (~> 3.5.0)
24
- rspec-mocks (~> 3.5.0)
25
- rspec-core (3.5.4)
26
- rspec-support (~> 3.5.0)
27
- rspec-expectations (3.5.0)
17
+ diff-lcs (1.4.4)
18
+ docile (1.4.0)
19
+ json (2.3.1)
20
+ rake (13.0.6)
21
+ redis (4.4.0)
22
+ rspec (3.10.0)
23
+ rspec-core (~> 3.10.0)
24
+ rspec-expectations (~> 3.10.0)
25
+ rspec-mocks (~> 3.10.0)
26
+ rspec-core (3.10.1)
27
+ rspec-support (~> 3.10.0)
28
+ rspec-expectations (3.10.1)
28
29
  diff-lcs (>= 1.2.0, < 2.0)
29
- rspec-support (~> 3.5.0)
30
- rspec-mocks (3.5.0)
30
+ rspec-support (~> 3.10.0)
31
+ rspec-mocks (3.10.2)
31
32
  diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.5.0)
33
- rspec-support (3.5.0)
33
+ rspec-support (~> 3.10.0)
34
+ rspec-support (3.10.2)
34
35
  simplecov (0.16.1)
35
36
  docile (~> 1.1)
36
37
  json (>= 1.8, < 3)
37
38
  simplecov-html (~> 0.10.0)
38
39
  simplecov-html (0.10.2)
39
- term-ansicolor (1.6.0)
40
+ sync (0.5.0)
41
+ term-ansicolor (1.7.1)
40
42
  tins (~> 1.0)
41
- thor (0.19.4)
42
- tins (1.16.3)
43
+ thor (1.1.0)
44
+ tins (1.29.1)
45
+ sync
43
46
 
44
47
  PLATFORMS
45
48
  ruby
46
49
 
47
50
  DEPENDENCIES
51
+ connection_pool (~> 2.2)
48
52
  coveralls (~> 0.8)
49
- rake (~> 11.1, >= 11.1.2)
53
+ json (~> 2.3.1, >= 2.3.0)
54
+ rake (~> 13.0, >= 11.1.2)
50
55
  redlock!
51
56
  rspec (~> 3, >= 3.0.0)
52
57
 
53
58
  BUNDLED WITH
54
- 1.17.2
59
+ 2.2.22
data/Makefile CHANGED
@@ -7,3 +7,6 @@ build:
7
7
 
8
8
  publish:
9
9
  docker-compose run --rm test gem push `ls -lt *gem | head -n 1 | awk '{ print $$9 }'`
10
+
11
+ updateLock:
12
+ docker-compose run --rm test bundle lock --update
data/README.md CHANGED
@@ -1,11 +1,9 @@
1
- [![Stories in Ready](https://badge.waffle.io/leandromoreira/redlock-rb.png?label=ready&title=Ready)](https://waffle.io/leandromoreira/redlock-rb)
2
1
  [![Build Status](https://travis-ci.org/leandromoreira/redlock-rb.svg?branch=master)](https://travis-ci.org/leandromoreira/redlock-rb)
3
2
  [![Coverage Status](https://coveralls.io/repos/leandromoreira/redlock-rb/badge.svg?branch=master)](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
4
3
  [![Code Climate](https://codeclimate.com/github/leandromoreira/redlock-rb/badges/gpa.svg)](https://codeclimate.com/github/leandromoreira/redlock-rb)
5
4
  [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
6
5
  [![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
7
6
  [![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
8
- [![Join the chat at https://gitter.im/leandromoreira/redlock-rb](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/leandromoreira/redlock-rb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9
7
 
10
8
 
11
9
  # Redlock - A ruby distributed lock using redis.
@@ -108,10 +106,72 @@ rescue Redlock::LockError
108
106
  end
109
107
  ```
110
108
 
111
- The above code will also acquire the lock if the previous lock has expired and the lock is currently free. Keep in mind that this means the lock could have been acquired by someone else in the meantime. To only extend the life of the lock if currently locked by yourself, use the `extend_life` parameter:
109
+ The above code will also acquire the lock if the previous lock has expired and the lock is currently free. Keep in mind that this means the lock could have been acquired and released by someone else in the meantime. To only extend the life of the lock if currently locked by yourself, use the `extend_only_if_locked` parameter:
112
110
 
113
111
  ```ruby
114
- lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
112
+ lock_manager.lock("resource key", 3000, extend: lock_info, extend_only_if_locked: true)
113
+ ```
114
+
115
+ ### Querying lock status
116
+
117
+ You can check if a resource is locked:
118
+
119
+ ```ruby
120
+ resource = "resource_key"
121
+ lock_info = lock_manager.lock(resource, 2000)
122
+ lock_manager.locked?(resource)
123
+ #=> true
124
+
125
+ lock_manager.unlock(lock_info)
126
+ lock_manager.locked?(resource)
127
+ #=> false
128
+ ```
129
+
130
+ Any caller can call the above method to query the status. If you hold a lock and would like to check if it is valid, you can use the `valid_lock?` method:
131
+
132
+ ```ruby
133
+ lock_info = lock_manager.lock("resource_key", 2000)
134
+ lock_manager.valid_lock?(lock_info)
135
+ #=> true
136
+
137
+ lock_manager.unlock(lock_info)
138
+ lock_manager.valid_lock?(lock_info)
139
+ #=> false
140
+ ```
141
+
142
+ The above methods **are not safe if you are using this to time critical code**, since they return true if the lock has not expired, even if there's only (for example) 1ms left on the lock. If you want to safely time the lock validity, you can use the `get_remaining_ttl_for_lock` and `get_remaining_ttl_for_resource` methods.
143
+
144
+ Use `get_remaining_ttl_for_lock` if you hold a lock and want to check the TTL specifically for your lock:
145
+ ```ruby
146
+ resource = "resource_key"
147
+ lock_info = lock_manager.lock(resource, 2000)
148
+ sleep 1
149
+
150
+ lock_manager.get_remaining_ttl_for_lock(lock_info)
151
+ #=> 986
152
+
153
+ lock_manager.unlock(lock_info)
154
+ lock_manager.get_remaining_ttl_for_lock(lock_info)
155
+ #=> nil
156
+ ```
157
+
158
+ Use `get_remaining_ttl_for_resource` if you do not hold a lock, but want to know the remaining TTL on a locked resource:
159
+ ```ruby
160
+ # Some part of the code
161
+ resource = "resource_key"
162
+ lock_info = lock_manager.lock(resource, 2000)
163
+
164
+ # Some other part of the code
165
+ lock_manager.locked?(resource)
166
+ #=> true
167
+ lock_manager.get_remaining_ttl_for_resource(resource)
168
+ #=> 1975
169
+
170
+ # Sometime later
171
+ lock_manager.locked?(resource)
172
+ #=> false
173
+ lock_manager.get_remaining_ttl_for_resource(resource)
174
+ #=> nil
115
175
  ```
116
176
 
117
177
  ## Redis client configuration
@@ -139,6 +199,14 @@ It's possible to customize the retry logic providing the following options:
139
199
  })
140
200
  ```
141
201
 
202
+ It is possible to associate `:retry_delay` option with `Proc` object. It will be called every time, with attempt number
203
+ as argument, to get delay time value before next retry.
204
+
205
+ ```ruby
206
+ retry_delay = proc { |attempt_number| 200 * attempt_number ** 2 } # delay of 200ms for 1st retry, 800ms for 2nd retry, etc.
207
+ lock_manager = Redlock::Client.new(servers, retry_delay: retry_delay)
208
+ ```
209
+
142
210
  For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
143
211
 
144
212
  ## Run tests
data/docker-compose.yml CHANGED
@@ -11,17 +11,23 @@ services:
11
11
  - REDIS1_PORT=6379
12
12
  - REDIS2_HOST=redis2.local.com
13
13
  - REDIS2_PORT=6379
14
+ - REDIS3_HOST=redis3.local.com
15
+ - REDIS3_PORT=6379
14
16
  - DEFAULT_REDIS_HOST=redis1.local.com
15
17
  - DEFAULT_REDIS_PORT=6379
16
18
  links:
17
19
  - redis1:redis1.local.com
18
20
  - redis2:redis2.local.com
21
+ - redis3:redis3.local.com
19
22
  depends_on:
20
23
  - redis1
21
24
  - redis2
25
+ - redis3
22
26
 
23
27
  redis1:
24
28
  image: redis
25
29
  redis2:
26
30
  image: redis
31
+ redis3:
32
+ image: redis
27
33
 
@@ -2,6 +2,8 @@ require 'redis'
2
2
  require 'securerandom'
3
3
 
4
4
  module Redlock
5
+ include Scripts
6
+
5
7
  class Client
6
8
  DEFAULT_REDIS_HOST = ENV["DEFAULT_REDIS_HOST"] || "localhost"
7
9
  DEFAULT_REDIS_PORT = ENV["DEFAULT_REDIS_PORT"] || "6379"
@@ -54,12 +56,25 @@ module Redlock
54
56
  # +resource+:: the resource (or key) string to be locked.
55
57
  # +ttl+:: The time-to-live in ms for the lock.
56
58
  # +options+:: Hash of optional parameters
59
+ # * +retry_count+: see +initialize+
60
+ # * +retry_delay+: see +initialize+
61
+ # * +retry_jitter+: see +initialize+
57
62
  # * +extend+: A lock ("lock_info") to extend.
58
- # * +extend_only_if_life+: If +extend+ is given, only acquire lock if currently held
63
+ # * +extend_only_if_locked+: Boolean, if +extend+ is given, only acquire lock if currently held
64
+ # * +extend_only_if_life+: Deprecated, same as +extend_only_if_locked+
65
+ # * +extend_life+: Deprecated, same as +extend_only_if_locked+
59
66
  # +block+:: an optional block to be executed; after its execution, the lock (if successfully
60
67
  # acquired) is automatically unlocked.
61
68
  def lock(resource, ttl, options = {}, &block)
62
69
  lock_info = try_lock_instances(resource, ttl, options)
70
+ if options[:extend_only_if_life] && !Gem::Deprecate.skip
71
+ warn 'DEPRECATION WARNING: The `extend_only_if_life` option has been renamed `extend_only_if_locked`.'
72
+ options[:extend_only_if_locked] = options[:extend_only_if_life]
73
+ end
74
+ if options[:extend_life] && !Gem::Deprecate.skip
75
+ warn 'DEPRECATION WARNING: The `extend_life` option has been renamed `extend_only_if_locked`.'
76
+ options[:extend_only_if_locked] = options[:extend_life]
77
+ end
63
78
 
64
79
  if block_given?
65
80
  begin
@@ -83,46 +98,77 @@ module Redlock
83
98
  # Locks a resource, executing the received block only after successfully acquiring the lock,
84
99
  # and returning its return value as a result.
85
100
  # See Redlock::Client#lock for parameters.
86
- def lock!(*args)
101
+ def lock!(resource, *args)
87
102
  fail 'No block passed' unless block_given?
88
103
 
89
- lock(*args) do |lock_info|
90
- raise LockError, 'failed to acquire lock' unless lock_info
104
+ lock(resource, *args) do |lock_info|
105
+ raise LockError, resource unless lock_info
91
106
  return yield
92
107
  end
93
108
  end
94
109
 
110
+ # Gets remaining ttl of a resource. The ttl is returned if the holder
111
+ # currently holds the lock and it has not expired, otherwise the method
112
+ # returns nil.
113
+ # Params:
114
+ # +lock_info+:: the lock that has been acquired when you locked the resource
115
+ def get_remaining_ttl_for_lock(lock_info)
116
+ ttl_info = try_get_remaining_ttl(lock_info[:resource])
117
+ return nil if ttl_info.nil? || ttl_info[:value] != lock_info[:value]
118
+ ttl_info[:ttl]
119
+ end
120
+
121
+ # Gets remaining ttl of a resource. If there is no valid lock, the method
122
+ # returns nil.
123
+ # Params:
124
+ # +resource+:: the name of the resource (string) for which to check the ttl
125
+ def get_remaining_ttl_for_resource(resource)
126
+ ttl_info = try_get_remaining_ttl(resource)
127
+ return nil if ttl_info.nil?
128
+ ttl_info[:ttl]
129
+ end
130
+
131
+ # Checks if a resource is locked
132
+ # Params:
133
+ # +lock_info+:: the lock that has been acquired when you locked the resource
134
+ def locked?(resource)
135
+ ttl = get_remaining_ttl_for_resource(resource)
136
+ !(ttl.nil? || ttl.zero?)
137
+ end
138
+
139
+ # Checks if a lock is still valid
140
+ # Params:
141
+ # +lock_info+:: the lock that has been acquired when you locked the resource
142
+ def valid_lock?(lock_info)
143
+ ttl = get_remaining_ttl_for_lock(lock_info)
144
+ !(ttl.nil? || ttl.zero?)
145
+ end
146
+
95
147
  private
96
148
 
97
149
  class RedisInstance
98
- UNLOCK_SCRIPT = <<-eos
99
- if redis.call("get",KEYS[1]) == ARGV[1] then
100
- return redis.call("del",KEYS[1])
101
- else
102
- return 0
103
- end
104
- eos
105
-
106
- # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
107
- # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
108
- # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
109
- LOCK_SCRIPT = <<-eos
110
- if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
111
- return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
150
+ module ConnectionPoolLike
151
+ def with
152
+ yield self
112
153
  end
113
- eos
154
+ end
114
155
 
115
156
  def initialize(connection)
116
- if connection.respond_to?(:client)
157
+ if connection.respond_to?(:with)
117
158
  @redis = connection
118
159
  else
119
- @redis = Redis.new(connection)
160
+ if connection.respond_to?(:client)
161
+ @redis = connection
162
+ else
163
+ @redis = Redis.new(connection)
164
+ end
165
+ @redis.extend(ConnectionPoolLike)
120
166
  end
121
167
  end
122
168
 
123
169
  def lock(resource, val, ttl, allow_new_lock)
124
170
  recover_from_script_flush do
125
- @redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
171
+ @redis.with { |conn| conn.evalsha Scripts::LOCK_SCRIPT_SHA, keys: [resource], argv: [val, ttl, allow_new_lock] }
126
172
  end
127
173
  rescue Redis::BaseConnectionError
128
174
  false
@@ -130,17 +176,32 @@ module Redlock
130
176
 
131
177
  def unlock(resource, val)
132
178
  recover_from_script_flush do
133
- @redis.evalsha @unlock_script_sha, keys: [resource], argv: [val]
179
+ @redis.with { |conn| conn.evalsha Scripts::UNLOCK_SCRIPT_SHA, keys: [resource], argv: [val] }
134
180
  end
135
181
  rescue
136
182
  # Nothing to do, unlocking is just a best-effort attempt.
137
183
  end
138
184
 
185
+ def get_remaining_ttl(resource)
186
+ recover_from_script_flush do
187
+ @redis.with { |conn| conn.evalsha Scripts::PTTL_SCRIPT_SHA, keys: [resource] }
188
+ end
189
+ rescue Redis::BaseConnectionError
190
+ nil
191
+ end
192
+
139
193
  private
140
194
 
141
195
  def load_scripts
142
- @unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
143
- @lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
196
+ scripts = [
197
+ Scripts::UNLOCK_SCRIPT,
198
+ Scripts::LOCK_SCRIPT,
199
+ Scripts::PTTL_SCRIPT
200
+ ]
201
+
202
+ scripts.each do |script|
203
+ @redis.with { |conn| conn.script(:load, script) }
204
+ end
144
205
  end
145
206
 
146
207
  def recover_from_script_flush
@@ -163,11 +224,12 @@ module Redlock
163
224
  end
164
225
 
165
226
  def try_lock_instances(resource, ttl, options)
166
- tries = options[:extend] ? 1 : (@retry_count + 1)
227
+ retry_count = options[:retry_count] || @retry_count
228
+ tries = options[:extend] ? 1 : (retry_count + 1)
167
229
 
168
230
  tries.times do |attempt_number|
169
231
  # Wait a random delay before retrying.
170
- sleep((@retry_delay + rand(@retry_jitter)).to_f / 1000) if attempt_number > 0
232
+ sleep(attempt_retry_delay(attempt_number, options)) if attempt_number > 0
171
233
 
172
234
  lock_info = lock_instances(resource, ttl, options)
173
235
  return lock_info if lock_info
@@ -176,9 +238,23 @@ module Redlock
176
238
  false
177
239
  end
178
240
 
241
+ def attempt_retry_delay(attempt_number, options)
242
+ retry_delay = options[:retry_delay] || @retry_delay
243
+ retry_jitter = options[:retry_jitter] || @retry_jitter
244
+
245
+ retry_delay =
246
+ if retry_delay.respond_to?(:call)
247
+ retry_delay.call(attempt_number)
248
+ else
249
+ retry_delay
250
+ end
251
+
252
+ (retry_delay + rand(retry_jitter)).to_f / 1000
253
+ end
254
+
179
255
  def lock_instances(resource, ttl, options)
180
256
  value = (options[:extend] || { value: SecureRandom.uuid })[:value]
181
- allow_new_lock = (options[:extend_life] || options[:extend_only_if_life]) ? 'no' : 'yes'
257
+ allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
182
258
 
183
259
  locked, time_elapsed = timed do
184
260
  @servers.select { |s| s.lock resource, value, ttl, allow_new_lock }.size
@@ -194,6 +270,37 @@ module Redlock
194
270
  end
195
271
  end
196
272
 
273
+ def try_get_remaining_ttl(resource)
274
+ # Responses from the servers are a 2 tuple of format [lock_value, ttl].
275
+ # The lock_value is nil if it does not exist. Since servers may have
276
+ # different lock values, the responses are grouped by the lock_value and
277
+ # transofrmed into a hash: { lock_value1 => [ttl1, ttl2, ttl3],
278
+ # lock_value2 => [ttl4, tt5] }
279
+ ttls_by_value, time_elapsed = timed do
280
+ @servers.map { |s| s.get_remaining_ttl(resource) }
281
+ .select { |ttl_tuple| ttl_tuple&.first }
282
+ .group_by(&:first)
283
+ .transform_values { |ttl_tuples| ttl_tuples.map { |t| t.last } }
284
+ end
285
+
286
+ # Authoritative lock value is that which is returned by the majority of
287
+ # servers
288
+ authoritative_value, ttls =
289
+ ttls_by_value.max_by { |(lock_value, ttls)| ttls.length }
290
+
291
+ if ttls && ttls.size >= @quorum
292
+ # Return the minimum TTL of an N/2+1 selection. It will always be
293
+ # correct (it will guarantee that at least N/2+1 servers have a TTL that
294
+ # value or longer)
295
+ min_ttl = ttls.sort.last(@quorum).first
296
+ min_ttl = min_ttl - time_elapsed - drift(min_ttl)
297
+ { value: authoritative_value, ttl: min_ttl }
298
+ else
299
+ # No lock_value is authoritatively held for the resource
300
+ nil
301
+ end
302
+ end
303
+
197
304
  def drift(ttl)
198
305
  # Add 2 milliseconds to the drift to account for Redis expires
199
306
  # precision, which is 1 millisecond, plus 1 millisecond min drift
@@ -0,0 +1,34 @@
1
+ require 'digest'
2
+
3
+ module Redlock
4
+ module Scripts
5
+ UNLOCK_SCRIPT = <<-eos
6
+ if redis.call("get",KEYS[1]) == ARGV[1] then
7
+ return redis.call("del",KEYS[1])
8
+ else
9
+ return 0
10
+ end
11
+ eos
12
+
13
+ # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
14
+ # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
15
+ # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
16
+ LOCK_SCRIPT = <<-eos
17
+ if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
18
+ return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
19
+ end
20
+ eos
21
+
22
+ PTTL_SCRIPT = <<-eos
23
+ return { redis.call("get", KEYS[1]), redis.call("pttl", KEYS[1]) }
24
+ eos
25
+
26
+ # We do not want to load the scripts on every Redlock::Client initialization.
27
+ # Hence, we rely on Redis handing out SHA1 hashes of the cached scripts and
28
+ # pre-calculate them instead of loading the scripts unconditionally. If the scripts
29
+ # have not been cached on Redis, `recover_from_script_flush` has our backs.
30
+ UNLOCK_SCRIPT_SHA = Digest::SHA1.hexdigest(UNLOCK_SCRIPT)
31
+ LOCK_SCRIPT_SHA = Digest::SHA1.hexdigest(LOCK_SCRIPT)
32
+ PTTL_SCRIPT_SHA = Digest::SHA1.hexdigest(PTTL_SCRIPT)
33
+ end
34
+ end
@@ -1,17 +1,29 @@
1
+ require 'redlock'
2
+
1
3
  module Redlock
2
4
  class Client
3
- attr_writer :testing_mode
5
+ class << self
6
+ attr_accessor :testing_mode
7
+ end
8
+
9
+ def testing_mode=(mode)
10
+ warn 'DEPRECATION WARNING: Instance-level `testing_mode` has been removed, and this ' +
11
+ 'setter will be removed in the future. Please set the testing mode on the `Redlock::Client` ' +
12
+ 'instead, e.g. `Redlock::Client.testing_mode = :bypass`.'
13
+
14
+ self.class.testing_mode = mode
15
+ end
4
16
 
5
17
  alias_method :try_lock_instances_without_testing, :try_lock_instances
6
18
 
7
19
  def try_lock_instances(resource, ttl, options)
8
- if @testing_mode == :bypass
20
+ if self.class.testing_mode == :bypass
9
21
  {
10
22
  validity: ttl,
11
23
  resource: resource,
12
24
  value: options[:extend] ? options[:extend].fetch(:value) : SecureRandom.uuid
13
25
  }
14
- elsif @testing_mode == :fail
26
+ elsif self.class.testing_mode == :fail
15
27
  false
16
28
  else
17
29
  try_lock_instances_without_testing resource, ttl, options
@@ -21,14 +33,14 @@ module Redlock
21
33
  alias_method :unlock_without_testing, :unlock
22
34
 
23
35
  def unlock(lock_info)
24
- unlock_without_testing lock_info unless @testing_mode == :bypass
36
+ unlock_without_testing lock_info unless self.class.testing_mode == :bypass
25
37
  end
26
38
 
27
39
  class RedisInstance
28
40
  alias_method :load_scripts_without_testing, :load_scripts
29
41
 
30
42
  def load_scripts
31
- load_scripts_without_testing
43
+ load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
32
44
  rescue Redis::CommandError
33
45
  # FakeRedis doesn't have #script, but doesn't need it either.
34
46
  raise unless defined?(::FakeRedis)
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '1.0.1'
2
+ VERSION = '1.2.2'
3
3
  end
data/lib/redlock.rb CHANGED
@@ -2,6 +2,11 @@ require 'redlock/version'
2
2
 
3
3
  module Redlock
4
4
  autoload :Client, 'redlock/client'
5
+ autoload :Scripts, 'redlock/scripts'
5
6
 
6
- LockError = Class.new(StandardError)
7
+ class LockError < StandardError
8
+ def initialize(resource)
9
+ super "failed to acquire lock on '#{resource}'".freeze
10
+ end
11
+ end
7
12
  end
data/redlock.gemspec CHANGED
@@ -1,26 +1,28 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'redlock/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "redlock"
8
+ spec.name = 'redlock'
8
9
  spec.version = Redlock::VERSION
9
- spec.authors = ["Leandro Moreira"]
10
- spec.email = ["leandro.ribeiro.moreira@gmail.com"]
11
- spec.summary = %q{Distributed lock using Redis written in Ruby.}
12
- spec.description = %q{Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.}
13
- spec.homepage = "https://github.com/leandromoreira/redlock-rb"
10
+ spec.authors = ['Leandro Moreira']
11
+ spec.email = ['leandro.ribeiro.moreira@gmail.com']
12
+ spec.summary = 'Distributed lock using Redis written in Ruby.'
13
+ spec.description = 'Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.'
14
+ spec.homepage = 'https://github.com/leandromoreira/redlock-rb'
14
15
  spec.license = 'BSD-2-Clause'
15
16
 
16
17
  spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
19
+ spec.require_paths = ['lib']
20
20
 
21
21
  spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
22
22
 
23
- spec.add_development_dependency "coveralls", "~> 0.8"
24
- spec.add_development_dependency 'rake', '~> 11.1', '>= 11.1.2'
23
+ spec.add_development_dependency 'connection_pool', '~> 2.2'
24
+ spec.add_development_dependency 'coveralls', '~> 0.8'
25
+ spec.add_development_dependency 'json', '>= 2.3.0', '~> 2.3.1'
26
+ spec.add_development_dependency 'rake', '>= 11.1.2', '~> 13.0'
25
27
  spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
26
28
  end
data/spec/client_spec.rb CHANGED
@@ -1,18 +1,28 @@
1
1
  require 'spec_helper'
2
2
  require 'securerandom'
3
3
  require 'redis'
4
+ require 'connection_pool'
4
5
 
5
6
  RSpec.describe Redlock::Client do
6
7
  # It is recommended to have at least 3 servers in production
7
8
  let(:lock_manager_opts) { { retry_count: 3 } }
8
9
  let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
9
- let(:redis_client) { Redis.new }
10
+ let(:redis_client) { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
10
11
  let(:resource_key) { SecureRandom.hex(3) }
11
12
  let(:ttl) { 1000 }
12
13
  let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
13
14
  let(:redis1_port) { ENV["REDIS1_PORT"] || "6379" }
14
15
  let(:redis2_host) { ENV["REDIS2_HOST"] || "127.0.0.1" }
15
16
  let(:redis2_port) { ENV["REDIS2_PORT"] || "6379" }
17
+ let(:redis3_host) { ENV["REDIS3_HOST"] || "127.0.0.1" }
18
+ let(:redis3_port) { ENV["REDIS3_PORT"] || "6379" }
19
+ let(:unreachable_redis) {
20
+ redis = Redis.new(url: 'redis://localhost:46864')
21
+ def redis.with
22
+ yield self
23
+ end
24
+ redis
25
+ }
16
26
 
17
27
  describe 'initialize' do
18
28
  it 'accepts both redis URLs and Redis objects' do
@@ -25,6 +35,24 @@ RSpec.describe Redlock::Client do
25
35
 
26
36
  expect(redlock_servers).to match_array([redis1_host, redis2_host])
27
37
  end
38
+
39
+ it 'accepts ConnectionPool objects' do
40
+ pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
41
+ redlock = Redlock::Client.new([pool])
42
+
43
+ lock_info = lock_manager.lock(resource_key, ttl)
44
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
45
+ lock_manager.unlock(lock_info)
46
+ end
47
+
48
+ it 'does not load scripts' do
49
+ redis_client.script(:flush)
50
+
51
+ pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
52
+ redlock = Redlock::Client.new([pool])
53
+
54
+ expect(redis_client.info["number_of_cached_scripts"]).to eq("0")
55
+ end
28
56
  end
29
57
 
30
58
  describe 'lock' do
@@ -64,9 +92,9 @@ RSpec.describe Redlock::Client do
64
92
  end
65
93
  end
66
94
 
67
- context 'when extend_only_if_life flag is given' do
95
+ context 'when extend_only_if_locked flag is given' do
68
96
  it 'does not extend a non-existent lock' do
69
- @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: true)
97
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: true)
70
98
  expect(@lock_info).to eq(false)
71
99
  end
72
100
  end
@@ -76,14 +104,14 @@ RSpec.describe Redlock::Client do
76
104
  lock_info = lock_manager.lock(resource_key, ttl)
77
105
  expect(resource_key).to_not be_lockable(lock_manager, ttl)
78
106
 
79
- lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
107
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
80
108
  expect(lock_info).not_to be_nil
81
109
  expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
82
110
  end
83
111
 
84
- context 'when extend_only_if_life flag is not given' do
112
+ context 'when extend_only_if_locked flag is not given' do
85
113
  it "sets the given value when trying to extend a non-existent lock" do
86
- @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: false)
114
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: false)
87
115
  expect(@lock_info).to be_lock_info_for(resource_key)
88
116
  expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
89
117
  end
@@ -94,6 +122,28 @@ RSpec.describe Redlock::Client do
94
122
  second_attempt = lock_manager.lock(resource_key, ttl)
95
123
  expect(second_attempt).to eq(false)
96
124
  end
125
+
126
+ context 'when extend_life flag is given' do
127
+ it 'treats it as extend_only_if_locked but warns it is deprecated' do
128
+ ttl = 20_000
129
+ lock_info = lock_manager.lock(resource_key, ttl)
130
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
131
+ expect(lock_manager).to receive(:warn).with(/DEPRECATION WARNING: The `extend_life`/)
132
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
133
+ expect(lock_info).not_to be_nil
134
+ end
135
+ end
136
+
137
+ context 'when extend_only_if_life flag is given' do
138
+ it 'treats it as extend_only_if_locked but warns it is deprecated' do
139
+ ttl = 20_000
140
+ lock_info = lock_manager.lock(resource_key, ttl)
141
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
142
+ expect(lock_manager).to receive(:warn).with(/DEPRECATION WARNING: The `extend_only_if_life`/)
143
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_life: true)
144
+ expect(lock_info).not_to be_nil
145
+ end
146
+ end
97
147
  end
98
148
 
99
149
  context 'when lock is not available' do
@@ -138,13 +188,64 @@ RSpec.describe Redlock::Client do
138
188
  end.at_least(:once)
139
189
  lock_manager.lock(resource_key, ttl)
140
190
  end
191
+
192
+ it 'accepts retry_delay as proc' do
193
+ retry_delay = proc do |attempt_number|
194
+ expect(attempt_number).to eq(1)
195
+ 2000
196
+ end
197
+
198
+ lock_manager = Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, retry_count: 1, retry_delay: retry_delay)
199
+ another_lock_info = lock_manager.lock(resource_key, ttl)
200
+
201
+ expect(lock_manager).to receive(:sleep) do |sleep|
202
+ expect(sleep * 1000).to be_within(described_class::DEFAULT_RETRY_JITTER).of(2000)
203
+ end.exactly(:once)
204
+ lock_manager.lock(resource_key, ttl)
205
+ lock_manager.unlock(another_lock_info)
206
+ end
207
+
208
+ context 'when retry_count is given' do
209
+ it 'prioritizes the retry_count in option and tries up to \'retry_count\' + 1 times' do
210
+ retry_count = 1
211
+ expect(retry_count).not_to eq(lock_manager_opts[:retry_count])
212
+ expect(lock_manager).to receive(:lock_instances).exactly(retry_count + 1).times.and_return(false)
213
+ lock_manager.lock(resource_key, ttl, retry_count: retry_count)
214
+ end
215
+ end
216
+
217
+ context 'when retry_delay is given' do
218
+ it 'prioritizes the retry_delay in option and sleeps at least the specified retry_delay in milliseconds' do
219
+ retry_delay = 300
220
+ expect(retry_delay > described_class::DEFAULT_RETRY_DELAY).to eq(true)
221
+ expected_minimum = retry_delay
222
+
223
+ expect(lock_manager).to receive(:sleep) do |sleep|
224
+ expect(sleep).to satisfy { |value| value >= expected_minimum / 1000.to_f }
225
+ end.at_least(:once)
226
+ lock_manager.lock(resource_key, ttl, retry_delay: retry_delay)
227
+ end
228
+ end
229
+
230
+ context 'when retry_jitter is given' do
231
+ it 'prioritizes the retry_jitter in option and sleeps a maximum of retry_delay + retry_jitter in milliseconds' do
232
+ retry_jitter = 60
233
+ expect(retry_jitter > described_class::DEFAULT_RETRY_JITTER).to eq(true)
234
+
235
+ expected_maximum = described_class::DEFAULT_RETRY_DELAY + retry_jitter
236
+ expect(lock_manager).to receive(:sleep) do |sleep|
237
+ expect(sleep).to satisfy { |value| value < expected_maximum / 1000.to_f }
238
+ end.at_least(:once)
239
+ lock_manager.lock(resource_key, ttl, retry_jitter: retry_jitter)
240
+ end
241
+ end
141
242
  end
142
243
 
143
244
  context 'when a server goes away' do
144
245
  it 'does not raise an error on connection issues' do
145
246
  # We re-route the lock manager to a (hopefully) non-existent Redis URL.
146
247
  redis_instance = lock_manager.instance_variable_get(:@servers).first
147
- redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
248
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
148
249
 
149
250
  expect {
150
251
  expect(lock_manager.lock(resource_key, ttl)).to be_falsey
@@ -156,9 +257,10 @@ RSpec.describe Redlock::Client do
156
257
  it 'recovers from connection issues' do
157
258
  # Same as above.
158
259
  redis_instance = lock_manager.instance_variable_get(:@servers).first
159
- redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
260
+ old_redis = redis_instance.instance_variable_get(:@redis)
261
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
160
262
  expect(lock_manager.lock(resource_key, ttl)).to be_falsey
161
- redis_instance.instance_variable_set(:@redis, Redis.new(url: "redis://#{redis1_host}:#{redis1_port}"))
263
+ redis_instance.instance_variable_set(:@redis, old_redis)
162
264
  expect(lock_manager.lock(resource_key, ttl)).to be_truthy
163
265
  end
164
266
  end
@@ -294,7 +396,9 @@ RSpec.describe Redlock::Client do
294
396
  after { lock_manager.unlock(@another_lock_info) }
295
397
 
296
398
  it 'raises a LockError' do
297
- expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(Redlock::LockError)
399
+ expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(
400
+ Redlock::LockError, "failed to acquire lock on '#{resource_key}'"
401
+ )
298
402
  end
299
403
 
300
404
  it 'does not execute the block' do
@@ -308,6 +412,154 @@ RSpec.describe Redlock::Client do
308
412
  end
309
413
  end
310
414
 
415
+ describe 'get_remaining_ttl_for_resource' do
416
+ context 'when lock is valid' do
417
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
418
+
419
+ it 'gets the remaining ttl of a lock' do
420
+ ttl = 20_000
421
+ @lock_info = lock_manager.lock(resource_key, ttl)
422
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
423
+ expect(remaining_ttl).to be_within(300).of(ttl)
424
+ end
425
+
426
+ context 'when servers respond with varying ttls' do
427
+ let (:servers) {
428
+ [
429
+ "redis://#{redis1_host}:#{redis1_port}",
430
+ "redis://#{redis2_host}:#{redis2_port}",
431
+ "redis://#{redis3_host}:#{redis3_port}"
432
+ ]
433
+ }
434
+ let (:redlock) { Redlock::Client.new(servers) }
435
+ after(:each) { redlock.unlock(@lock_info) if @lock_info }
436
+
437
+ it 'returns the minimum ttl value' do
438
+ ttl = 20_000
439
+ @lock_info = redlock.lock(resource_key, ttl)
440
+
441
+ # Mock redis server responses to return different ttls
442
+ returned_ttls = [20_000, 15_000, 10_000]
443
+ redlock.instance_variable_get(:@servers).each_with_index do |server, index|
444
+ allow(server).to(receive(:get_remaining_ttl))
445
+ .with(resource_key)
446
+ .and_return([@lock_info[:value], returned_ttls[index]])
447
+ end
448
+
449
+ remaining_ttl = redlock.get_remaining_ttl_for_lock(@lock_info)
450
+
451
+ # Assert that the TTL is closest to the closest to the correct value
452
+ expect(remaining_ttl).to be_within(300).of(returned_ttls[1])
453
+ end
454
+ end
455
+ end
456
+
457
+ context 'when lock is not valid' do
458
+ it 'returns nil' do
459
+ lock_info = lock_manager.lock(resource_key, ttl)
460
+ lock_manager.unlock(lock_info)
461
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
462
+ expect(remaining_ttl).to be_nil
463
+ end
464
+ end
465
+
466
+ context 'when server goes away' do
467
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
468
+
469
+ it 'does not raise an error on connection issues' do
470
+ @lock_info = lock_manager.lock(resource_key, ttl)
471
+
472
+ # Replace redis with unreachable instance
473
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
474
+ old_redis = redis_instance.instance_variable_get(:@redis)
475
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
476
+
477
+ expect {
478
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
479
+ expect(remaining_ttl).to be_nil
480
+ }.to_not raise_error
481
+ end
482
+ end
483
+
484
+ context 'when a server comes back' do
485
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
486
+
487
+ it 'recovers from connection issues' do
488
+ @lock_info = lock_manager.lock(resource_key, ttl)
489
+
490
+ # Replace redis with unreachable instance
491
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
492
+ old_redis = redis_instance.instance_variable_get(:@redis)
493
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
494
+
495
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_nil
496
+
497
+ # Restore redis
498
+ redis_instance.instance_variable_set(:@redis, old_redis)
499
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_truthy
500
+ end
501
+ end
502
+ end
503
+
504
+ describe 'get_remaining_ttl_for_lock' do
505
+ context 'when lock is valid' do
506
+ it 'gets the remaining ttl of a lock' do
507
+ ttl = 20_000
508
+ lock_info = lock_manager.lock(resource_key, ttl)
509
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
510
+ expect(remaining_ttl).to be_within(300).of(ttl)
511
+ lock_manager.unlock(lock_info)
512
+ end
513
+ end
514
+
515
+ context 'when lock is not valid' do
516
+ it 'returns nil' do
517
+ lock_info = lock_manager.lock(resource_key, ttl)
518
+ lock_manager.unlock(lock_info)
519
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
520
+ expect(remaining_ttl).to be_nil
521
+ end
522
+ end
523
+ end
524
+
525
+ describe 'locked?' do
526
+ context 'when lock is available' do
527
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
528
+
529
+ it 'returns true' do
530
+ @lock_info = lock_manager.lock(resource_key, ttl)
531
+ expect(lock_manager).to be_locked(resource_key)
532
+ end
533
+ end
534
+
535
+ context 'when lock is not available' do
536
+ it 'returns false' do
537
+ lock_info = lock_manager.lock(resource_key, ttl)
538
+ lock_manager.unlock(lock_info)
539
+ expect(lock_manager).not_to be_locked(resource_key)
540
+ end
541
+ end
542
+ end
543
+
544
+ describe 'valid_lock?' do
545
+ context 'when lock is available' do
546
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
547
+
548
+ it 'returns true' do
549
+ @lock_info = lock_manager.lock(resource_key, ttl)
550
+ expect(lock_manager).to be_valid_lock(@lock_info)
551
+ end
552
+ end
553
+
554
+ context 'when lock is not available' do
555
+ it 'returns false' do
556
+ lock_info = lock_manager.lock(resource_key, ttl)
557
+ lock_manager.unlock(lock_info)
558
+ expect(lock_manager).not_to be_valid_lock(lock_info)
559
+ end
560
+ end
561
+ end
562
+
311
563
  describe '#default_time_source' do
312
564
  context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
313
565
  it 'returns a callable using Process.clock_gettime()' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redlock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Moreira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-29 00:00:00.000000000 Z
11
+ date: 2021-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '5.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: connection_pool
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.2'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.2'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: coveralls
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -45,45 +59,65 @@ dependencies:
45
59
  - !ruby/object:Gem::Version
46
60
  version: '0.8'
47
61
  - !ruby/object:Gem::Dependency
48
- name: rake
62
+ name: json
49
63
  requirement: !ruby/object:Gem::Requirement
50
64
  requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.3.0
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: 2.3.1
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: 2.3.0
51
78
  - - "~>"
52
79
  - !ruby/object:Gem::Version
53
- version: '11.1'
80
+ version: 2.3.1
81
+ - !ruby/object:Gem::Dependency
82
+ name: rake
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
54
85
  - - ">="
55
86
  - !ruby/object:Gem::Version
56
87
  version: 11.1.2
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '13.0'
57
91
  type: :development
58
92
  prerelease: false
59
93
  version_requirements: !ruby/object:Gem::Requirement
60
94
  requirements:
61
- - - "~>"
62
- - !ruby/object:Gem::Version
63
- version: '11.1'
64
95
  - - ">="
65
96
  - !ruby/object:Gem::Version
66
97
  version: 11.1.2
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '13.0'
67
101
  - !ruby/object:Gem::Dependency
68
102
  name: rspec
69
103
  requirement: !ruby/object:Gem::Requirement
70
104
  requirements:
71
- - - ">="
72
- - !ruby/object:Gem::Version
73
- version: 3.0.0
74
105
  - - "~>"
75
106
  - !ruby/object:Gem::Version
76
107
  version: '3'
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 3.0.0
77
111
  type: :development
78
112
  prerelease: false
79
113
  version_requirements: !ruby/object:Gem::Requirement
80
114
  requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: 3.0.0
84
115
  - - "~>"
85
116
  - !ruby/object:Gem::Version
86
117
  version: '3'
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 3.0.0
87
121
  description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
88
122
  email:
89
123
  - leandro.ribeiro.moreira@gmail.com
@@ -105,6 +139,7 @@ files:
105
139
  - docker-compose.yml
106
140
  - lib/redlock.rb
107
141
  - lib/redlock/client.rb
142
+ - lib/redlock/scripts.rb
108
143
  - lib/redlock/testing.rb
109
144
  - lib/redlock/version.rb
110
145
  - redlock.gemspec
@@ -130,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
165
  - !ruby/object:Gem::Version
131
166
  version: '0'
132
167
  requirements: []
133
- rubygems_version: 3.0.3
168
+ rubygems_version: 3.2.22
134
169
  signing_key:
135
170
  specification_version: 4
136
171
  summary: Distributed lock using Redis written in Ruby.