redlock 1.0.0 → 2.0.0

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
- SHA1:
3
- metadata.gz: 12311731641f60b7873ced7fcb039d073fc21525
4
- data.tar.gz: 58bc941bc7bbebc10b3fcc32a32e970c3abcefe8
2
+ SHA256:
3
+ metadata.gz: 679d1c44fcda7a2eaa70bdfe38c30e4ee79e417bef0b3ce064aceaf0a9cc0bbb
4
+ data.tar.gz: 48b6a41c4cc27b8ff8878647dd494a6b87e3b17e540b75d8699e32d4ee982aa5
5
5
  SHA512:
6
- metadata.gz: 719b17fb2701e48d3f574f198bafd078fd0f9b6ec91f686e4ece547e222c1b4bf09f5ee94868f1d1417c2e6c87d5c3b4742a185c13bb005fc83d868b2b5d0bc1
7
- data.tar.gz: cc759db7fe3ba5f8cf09f1f1427564d1c338a97dc1959304e7fdc561f2817e5f133a54b381382ca1b96712e3109f41247ffa5da4a40bf6fd05a8bfb2d875832b
6
+ metadata.gz: a31815a7da2fa6f5c3af35734294cdb34fdfba19dc12eaf25abe950db65be1356afc397ff29a73823d267d99d0069f86651385c1e262da6ced7541345212abda
7
+ data.tar.gz: cd4e41fe60732180b4b5f3d848715954074f8a063c1513c89e73a37311d5d4c0d8fbe1bbd92b063955324929f07bf59232040de104bfeb14d18837d297910d59
@@ -0,0 +1,31 @@
1
+ name: Ruby CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ ruby-version: [3.1, "3.0", "2.7", "2.6", "2.5", "ruby-head"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Set up Ruby ${{ matrix.ruby-version }}
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby-version }}
24
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
25
+ - name: Start Redis
26
+ uses: supercharge/redis-github-action@1.2.0
27
+ with:
28
+ redis-version: 6
29
+ - name: Run tests
30
+ run: bundle exec rspec
31
+
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  *.gem
2
2
  coverage/
3
3
  .bundle/
4
+ dump.rdb
5
+ Gemfile.lock
data/Makefile CHANGED
@@ -2,3 +2,8 @@ default: test
2
2
  test:
3
3
  docker-compose run --rm test
4
4
 
5
+ build:
6
+ docker-compose run --rm test gem build redlock.gemspec
7
+
8
+ publish:
9
+ docker-compose run --rm test gem push `ls -lt *gem | head -n 1 | awk '{ print $$9 }'`
data/README.md CHANGED
@@ -1,12 +1,8 @@
1
- [![Stories in Ready](https://badge.waffle.io/leandromoreira/redlock-rb.png?label=ready&title=Ready)](https://waffle.io/leandromoreira/redlock-rb)
2
- [![Build Status](https://travis-ci.org/leandromoreira/redlock-rb.svg?branch=master)](https://travis-ci.org/leandromoreira/redlock-rb)
1
+ [![Build Status](https://github.com/leandromoreira/redlock-rb/actions/workflows/ci.yml/badge.svg)](https://github.com/leandromoreira/redlock-rb/actions/workflows/ci.yml)
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
- [![Dependency Status](https://gemnasium.com/leandromoreira/redlock-rb.svg)](https://gemnasium.com/leandromoreira/redlock-rb)
6
4
  [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
7
- [![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
8
5
  [![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
9
- [![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)
10
6
 
11
7
 
12
8
  # Redlock - A ruby distributed lock using redis.
@@ -19,7 +15,7 @@ This is an implementation of a proposed [distributed lock algorithm with Redis](
19
15
 
20
16
  ## Compatibility
21
17
 
22
- Redlock works with Redis versions 2.6 or later.
18
+ Redlock works with Redis versions 6.0 or later.
23
19
 
24
20
  ## Installation
25
21
 
@@ -45,6 +41,7 @@ Or install it yourself as:
45
41
 
46
42
  ### Acquiring a lock
47
43
 
44
+ NOTE: All expiration durations are in milliseconds.
48
45
  ```ruby
49
46
  # Locking
50
47
  lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
@@ -108,10 +105,72 @@ rescue Redlock::LockError
108
105
  end
109
106
  ```
110
107
 
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:
108
+ 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
109
 
113
110
  ```ruby
114
- lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
111
+ lock_manager.lock("resource key", 3000, extend: lock_info, extend_only_if_locked: true)
112
+ ```
113
+
114
+ ### Querying lock status
115
+
116
+ You can check if a resource is locked:
117
+
118
+ ```ruby
119
+ resource = "resource_key"
120
+ lock_info = lock_manager.lock(resource, 2000)
121
+ lock_manager.locked?(resource)
122
+ #=> true
123
+
124
+ lock_manager.unlock(lock_info)
125
+ lock_manager.locked?(resource)
126
+ #=> false
127
+ ```
128
+
129
+ 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:
130
+
131
+ ```ruby
132
+ lock_info = lock_manager.lock("resource_key", 2000)
133
+ lock_manager.valid_lock?(lock_info)
134
+ #=> true
135
+
136
+ lock_manager.unlock(lock_info)
137
+ lock_manager.valid_lock?(lock_info)
138
+ #=> false
139
+ ```
140
+
141
+ 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.
142
+
143
+ Use `get_remaining_ttl_for_lock` if you hold a lock and want to check the TTL specifically for your lock:
144
+ ```ruby
145
+ resource = "resource_key"
146
+ lock_info = lock_manager.lock(resource, 2000)
147
+ sleep 1
148
+
149
+ lock_manager.get_remaining_ttl_for_lock(lock_info)
150
+ #=> 986
151
+
152
+ lock_manager.unlock(lock_info)
153
+ lock_manager.get_remaining_ttl_for_lock(lock_info)
154
+ #=> nil
155
+ ```
156
+
157
+ Use `get_remaining_ttl_for_resource` if you do not hold a lock, but want to know the remaining TTL on a locked resource:
158
+ ```ruby
159
+ # Some part of the code
160
+ resource = "resource_key"
161
+ lock_info = lock_manager.lock(resource, 2000)
162
+
163
+ # Some other part of the code
164
+ lock_manager.locked?(resource)
165
+ #=> true
166
+ lock_manager.get_remaining_ttl_for_resource(resource)
167
+ #=> 1975
168
+
169
+ # Sometime later
170
+ lock_manager.locked?(resource)
171
+ #=> false
172
+ lock_manager.get_remaining_ttl_for_resource(resource)
173
+ #=> nil
115
174
  ```
116
175
 
117
176
  ## Redis client configuration
@@ -119,7 +178,7 @@ lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
119
178
  `Redlock::Client` expects URLs or Redis objects on initialization. Redis objects should be used for configuring the connection in more detail, i.e. setting username and password.
120
179
 
121
180
  ```ruby
122
- servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
181
+ servers = [ 'redis://localhost:6379', RedisClient.new(:url => 'redis://someotherhost:6379') ]
123
182
  redlock = Redlock::Client.new(servers)
124
183
  ```
125
184
 
@@ -139,6 +198,14 @@ It's possible to customize the retry logic providing the following options:
139
198
  })
140
199
  ```
141
200
 
201
+ It is possible to associate `:retry_delay` option with `Proc` object. It will be called every time, with attempt number
202
+ as argument, to get delay time value before next retry.
203
+
204
+ ```ruby
205
+ retry_delay = proc { |attempt_number| 200 * attempt_number ** 2 } # delay of 200ms for 1st retry, 800ms for 2nd retry, etc.
206
+ lock_manager = Redlock::Client.new(servers, retry_delay: retry_delay)
207
+ ```
208
+
142
209
  For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
143
210
 
144
211
  ## 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
 
@@ -1,7 +1,9 @@
1
- require 'redis'
1
+ require 'redis-client'
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,73 +98,117 @@ 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
150
+ module ConnectionPoolLike
151
+ def with
152
+ yield self
103
153
  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])
112
- 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 = RedisClient.new(connection)
164
+ end
165
+ @redis.extend(ConnectionPoolLike)
120
166
  end
121
-
122
- load_scripts
123
167
  end
124
168
 
125
169
  def lock(resource, val, ttl, allow_new_lock)
126
170
  recover_from_script_flush do
127
- @redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
171
+ @redis.call('EVALSHA', Scripts::LOCK_SCRIPT_SHA, 1, resource, val, ttl, allow_new_lock)
128
172
  end
129
- rescue Redis::CannotConnectError
130
- false
131
173
  end
132
174
 
133
175
  def unlock(resource, val)
134
176
  recover_from_script_flush do
135
- @redis.evalsha @unlock_script_sha, keys: [resource], argv: [val]
177
+ @redis.call('EVALSHA', Scripts::UNLOCK_SCRIPT_SHA, 1, resource, val)
136
178
  end
137
179
  rescue
138
180
  # Nothing to do, unlocking is just a best-effort attempt.
139
181
  end
140
182
 
183
+ def get_remaining_ttl(resource)
184
+ recover_from_script_flush do
185
+ @redis.call('EVALSHA', Scripts::PTTL_SCRIPT_SHA, 1, resource)
186
+ end
187
+ rescue RedisClient::ConnectionError
188
+ nil
189
+ end
190
+
141
191
  private
142
192
 
143
193
  def load_scripts
144
- @unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
145
- @lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
194
+ scripts = [
195
+ Scripts::UNLOCK_SCRIPT,
196
+ Scripts::LOCK_SCRIPT,
197
+ Scripts::PTTL_SCRIPT
198
+ ]
199
+
200
+ @redis.with do |connnection|
201
+ scripts.each do |script|
202
+ connnection.call('SCRIPT', 'LOAD', script)
203
+ end
204
+ end
146
205
  end
147
206
 
148
207
  def recover_from_script_flush
149
208
  retry_on_noscript = true
150
209
  begin
151
210
  yield
152
- rescue Redis::CommandError => e
211
+ rescue RedisClient::CommandError => e
153
212
  # When somebody has flushed the Redis instance's script cache, we might
154
213
  # want to reload our scripts. Only attempt this once, though, to avoid
155
214
  # going into an infinite loop.
@@ -165,11 +224,12 @@ module Redlock
165
224
  end
166
225
 
167
226
  def try_lock_instances(resource, ttl, options)
168
- tries = options[:extend] ? 1 : (@retry_count + 1)
227
+ retry_count = options[:retry_count] || @retry_count
228
+ tries = options[:extend] ? 1 : (retry_count + 1)
169
229
 
170
230
  tries.times do |attempt_number|
171
231
  # Wait a random delay before retrying.
172
- 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
173
233
 
174
234
  lock_info = lock_instances(resource, ttl, options)
175
235
  return lock_info if lock_info
@@ -178,9 +238,23 @@ module Redlock
178
238
  false
179
239
  end
180
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
+
181
255
  def lock_instances(resource, ttl, options)
182
256
  value = (options[:extend] || { value: SecureRandom.uuid })[:value]
183
- allow_new_lock = (options[:extend_life] || options[:extend_only_if_life]) ? 'no' : 'yes'
257
+ allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
184
258
 
185
259
  locked, time_elapsed = timed do
186
260
  @servers.select { |s| s.lock resource, value, ttl, allow_new_lock }.size
@@ -196,6 +270,37 @@ module Redlock
196
270
  end
197
271
  end
198
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
+
199
304
  def drift(ttl)
200
305
  # Add 2 milliseconds to the drift to account for Redis expires
201
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,15 +33,15 @@ 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
32
- rescue Redis::CommandError
43
+ load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
44
+ rescue RedisClient::CommandError
33
45
  # FakeRedis doesn't have #script, but doesn't need it either.
34
46
  raise unless defined?(::FakeRedis)
35
47
  rescue NoMethodError
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '1.0.0'
2
+ VERSION = '2.0.0'
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
- spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
21
+ spec.add_dependency 'redis-client'
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,30 +1,60 @@
1
1
  require 'spec_helper'
2
2
  require 'securerandom'
3
- require 'redis'
3
+ require 'connection_pool'
4
4
 
5
5
  RSpec.describe Redlock::Client do
6
6
  # It is recommended to have at least 3 servers in production
7
7
  let(:lock_manager_opts) { { retry_count: 3 } }
8
8
  let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
9
- let(:redis_client) { Redis.new }
9
+ let(:redis_client) { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
10
10
  let(:resource_key) { SecureRandom.hex(3) }
11
11
  let(:ttl) { 1000 }
12
12
  let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
13
13
  let(:redis1_port) { ENV["REDIS1_PORT"] || "6379" }
14
14
  let(:redis2_host) { ENV["REDIS2_HOST"] || "127.0.0.1" }
15
15
  let(:redis2_port) { ENV["REDIS2_PORT"] || "6379" }
16
+ let(:redis3_host) { ENV["REDIS3_HOST"] || "127.0.0.1" }
17
+ let(:redis3_port) { ENV["REDIS3_PORT"] || "6379" }
18
+ let(:unreachable_redis) {
19
+ redis = RedisClient.new(url: 'redis://localhost:46864')
20
+ def redis.with
21
+ yield self
22
+ end
23
+ redis
24
+ }
16
25
 
17
26
  describe 'initialize' do
18
27
  it 'accepts both redis URLs and Redis objects' do
19
- servers = [ "redis://#{redis1_host}:#{redis1_port}", Redis.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
28
+ servers = [ "redis://#{redis1_host}:#{redis1_port}", RedisClient.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
20
29
  redlock = Redlock::Client.new(servers)
21
30
 
22
31
  redlock_servers = redlock.instance_variable_get(:@servers).map do |s|
23
- s.instance_variable_get(:@redis).connection[:host]
32
+ s.instance_variable_get(:@redis).config.host
24
33
  end
25
34
 
26
35
  expect(redlock_servers).to match_array([redis1_host, redis2_host])
27
36
  end
37
+
38
+ it 'accepts ConnectionPool objects' do
39
+ pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
40
+ redlock = Redlock::Client.new([pool])
41
+
42
+ lock_info = lock_manager.lock(resource_key, ttl)
43
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
44
+ lock_manager.unlock(lock_info)
45
+ end
46
+
47
+ it 'does not load scripts' do
48
+ redis_client.call('SCRIPT', 'FLUSH')
49
+
50
+ pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
51
+ redlock = Redlock::Client.new([pool])
52
+
53
+ raw_info = redis_client.call('INFO')
54
+ number_of_cached_scripts = raw_info[/number_of_cached_scripts\:\d+/].split(':').last
55
+
56
+ expect(number_of_cached_scripts).to eq("0")
57
+ end
28
58
  end
29
59
 
30
60
  describe 'lock' do
@@ -46,7 +76,7 @@ RSpec.describe Redlock::Client do
46
76
  it 'interprets lock time as milliseconds' do
47
77
  ttl = 20000
48
78
  @lock_info = lock_manager.lock(resource_key, ttl)
49
- expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
79
+ expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
50
80
  end
51
81
 
52
82
  it 'can extend its own lock' do
@@ -64,9 +94,9 @@ RSpec.describe Redlock::Client do
64
94
  end
65
95
  end
66
96
 
67
- context 'when extend_only_if_life flag is given' do
97
+ context 'when extend_only_if_locked flag is given' do
68
98
  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)
99
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: true)
70
100
  expect(@lock_info).to eq(false)
71
101
  end
72
102
  end
@@ -76,14 +106,14 @@ RSpec.describe Redlock::Client do
76
106
  lock_info = lock_manager.lock(resource_key, ttl)
77
107
  expect(resource_key).to_not be_lockable(lock_manager, ttl)
78
108
 
79
- lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
109
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
80
110
  expect(lock_info).not_to be_nil
81
- expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
111
+ expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
82
112
  end
83
113
 
84
- context 'when extend_only_if_life flag is not given' do
114
+ context 'when extend_only_if_locked flag is not given' do
85
115
  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)
116
+ @lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: false)
87
117
  expect(@lock_info).to be_lock_info_for(resource_key)
88
118
  expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
89
119
  end
@@ -94,6 +124,28 @@ RSpec.describe Redlock::Client do
94
124
  second_attempt = lock_manager.lock(resource_key, ttl)
95
125
  expect(second_attempt).to eq(false)
96
126
  end
127
+
128
+ context 'when extend_life flag is given' do
129
+ it 'treats it as extend_only_if_locked but warns it is deprecated' do
130
+ ttl = 20_000
131
+ lock_info = lock_manager.lock(resource_key, ttl)
132
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
133
+ expect(lock_manager).to receive(:warn).with(/DEPRECATION WARNING: The `extend_life`/)
134
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
135
+ expect(lock_info).not_to be_nil
136
+ end
137
+ end
138
+
139
+ context 'when extend_only_if_life flag is given' do
140
+ it 'treats it as extend_only_if_locked but warns it is deprecated' do
141
+ ttl = 20_000
142
+ lock_info = lock_manager.lock(resource_key, ttl)
143
+ expect(resource_key).to_not be_lockable(lock_manager, ttl)
144
+ expect(lock_manager).to receive(:warn).with(/DEPRECATION WARNING: The `extend_only_if_life`/)
145
+ lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_life: true)
146
+ expect(lock_info).not_to be_nil
147
+ end
148
+ end
97
149
  end
98
150
 
99
151
  context 'when lock is not available' do
@@ -138,17 +190,68 @@ RSpec.describe Redlock::Client do
138
190
  end.at_least(:once)
139
191
  lock_manager.lock(resource_key, ttl)
140
192
  end
193
+
194
+ it 'accepts retry_delay as proc' do
195
+ retry_delay = proc do |attempt_number|
196
+ expect(attempt_number).to eq(1)
197
+ 2000
198
+ end
199
+
200
+ lock_manager = Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, retry_count: 1, retry_delay: retry_delay)
201
+ another_lock_info = lock_manager.lock(resource_key, ttl)
202
+
203
+ expect(lock_manager).to receive(:sleep) do |sleep|
204
+ expect(sleep * 1000).to be_within(described_class::DEFAULT_RETRY_JITTER).of(2000)
205
+ end.exactly(:once)
206
+ lock_manager.lock(resource_key, ttl)
207
+ lock_manager.unlock(another_lock_info)
208
+ end
209
+
210
+ context 'when retry_count is given' do
211
+ it 'prioritizes the retry_count in option and tries up to \'retry_count\' + 1 times' do
212
+ retry_count = 1
213
+ expect(retry_count).not_to eq(lock_manager_opts[:retry_count])
214
+ expect(lock_manager).to receive(:lock_instances).exactly(retry_count + 1).times.and_return(false)
215
+ lock_manager.lock(resource_key, ttl, retry_count: retry_count)
216
+ end
217
+ end
218
+
219
+ context 'when retry_delay is given' do
220
+ it 'prioritizes the retry_delay in option and sleeps at least the specified retry_delay in milliseconds' do
221
+ retry_delay = 300
222
+ expect(retry_delay > described_class::DEFAULT_RETRY_DELAY).to eq(true)
223
+ expected_minimum = retry_delay
224
+
225
+ expect(lock_manager).to receive(:sleep) do |sleep|
226
+ expect(sleep).to satisfy { |value| value >= expected_minimum / 1000.to_f }
227
+ end.at_least(:once)
228
+ lock_manager.lock(resource_key, ttl, retry_delay: retry_delay)
229
+ end
230
+ end
231
+
232
+ context 'when retry_jitter is given' do
233
+ it 'prioritizes the retry_jitter in option and sleeps a maximum of retry_delay + retry_jitter in milliseconds' do
234
+ retry_jitter = 60
235
+ expect(retry_jitter > described_class::DEFAULT_RETRY_JITTER).to eq(true)
236
+
237
+ expected_maximum = described_class::DEFAULT_RETRY_DELAY + retry_jitter
238
+ expect(lock_manager).to receive(:sleep) do |sleep|
239
+ expect(sleep).to satisfy { |value| value < expected_maximum / 1000.to_f }
240
+ end.at_least(:once)
241
+ lock_manager.lock(resource_key, ttl, retry_jitter: retry_jitter)
242
+ end
243
+ end
141
244
  end
142
245
 
143
246
  context 'when a server goes away' do
144
- it 'does not raise an error on connection issues' do
145
- # We re-route the lock manager to a (hopefully) non-existent Redis URL.
247
+ it 'raises an error on connection issues' do
248
+ # Set lock manager to a (hopefully) non-existent Redis URL to test error
146
249
  redis_instance = lock_manager.instance_variable_get(:@servers).first
147
- redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
250
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
148
251
 
149
252
  expect {
150
- expect(lock_manager.lock(resource_key, ttl)).to be_falsey
151
- }.to_not raise_error
253
+ lock_manager.lock(resource_key, ttl)
254
+ }.to raise_error(RedisClient::CannotConnectError)
152
255
  end
153
256
  end
154
257
 
@@ -156,9 +259,12 @@ RSpec.describe Redlock::Client do
156
259
  it 'recovers from connection issues' do
157
260
  # Same as above.
158
261
  redis_instance = lock_manager.instance_variable_get(:@servers).first
159
- redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
160
- 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}"))
262
+ old_redis = redis_instance.instance_variable_get(:@redis)
263
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
264
+ expect {
265
+ lock_manager.lock(resource_key, ttl)
266
+ }.to raise_error(RedisClient::CannotConnectError)
267
+ redis_instance.instance_variable_set(:@redis, old_redis)
162
268
  expect(lock_manager.lock(resource_key, ttl)).to be_truthy
163
269
  end
164
270
  end
@@ -166,10 +272,10 @@ RSpec.describe Redlock::Client do
166
272
  context 'when script cache has been flushed' do
167
273
  before(:each) do
168
274
  @manipulated_instance = lock_manager.instance_variable_get(:@servers).first
169
- @manipulated_instance.instance_variable_get(:@redis).script(:flush)
275
+ @manipulated_instance.instance_variable_get(:@redis).call('SCRIPT', 'FLUSH')
170
276
  end
171
277
 
172
- it 'does not raise a Redis::CommandError: NOSCRIPT error' do
278
+ it 'does not raise a RedisClient::CommandError: NOSCRIPT error' do
173
279
  expect {
174
280
  lock_manager.lock(resource_key, ttl)
175
281
  }.to_not raise_error
@@ -294,7 +400,9 @@ RSpec.describe Redlock::Client do
294
400
  after { lock_manager.unlock(@another_lock_info) }
295
401
 
296
402
  it 'raises a LockError' do
297
- expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(Redlock::LockError)
403
+ expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(
404
+ Redlock::LockError, "failed to acquire lock on '#{resource_key}'"
405
+ )
298
406
  end
299
407
 
300
408
  it 'does not execute the block' do
@@ -308,6 +416,154 @@ RSpec.describe Redlock::Client do
308
416
  end
309
417
  end
310
418
 
419
+ describe 'get_remaining_ttl_for_resource' do
420
+ context 'when lock is valid' do
421
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
422
+
423
+ it 'gets the remaining ttl of a lock' do
424
+ ttl = 20_000
425
+ @lock_info = lock_manager.lock(resource_key, ttl)
426
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
427
+ expect(remaining_ttl).to be_within(300).of(ttl)
428
+ end
429
+
430
+ context 'when servers respond with varying ttls' do
431
+ let (:servers) {
432
+ [
433
+ "redis://#{redis1_host}:#{redis1_port}",
434
+ "redis://#{redis2_host}:#{redis2_port}",
435
+ "redis://#{redis3_host}:#{redis3_port}"
436
+ ]
437
+ }
438
+ let (:redlock) { Redlock::Client.new(servers) }
439
+ after(:each) { redlock.unlock(@lock_info) if @lock_info }
440
+
441
+ it 'returns the minimum ttl value' do
442
+ ttl = 20_000
443
+ @lock_info = redlock.lock(resource_key, ttl)
444
+
445
+ # Mock redis server responses to return different ttls
446
+ returned_ttls = [20_000, 15_000, 10_000]
447
+ redlock.instance_variable_get(:@servers).each_with_index do |server, index|
448
+ allow(server).to(receive(:get_remaining_ttl))
449
+ .with(resource_key)
450
+ .and_return([@lock_info[:value], returned_ttls[index]])
451
+ end
452
+
453
+ remaining_ttl = redlock.get_remaining_ttl_for_lock(@lock_info)
454
+
455
+ # Assert that the TTL is closest to the closest to the correct value
456
+ expect(remaining_ttl).to be_within(300).of(returned_ttls[1])
457
+ end
458
+ end
459
+ end
460
+
461
+ context 'when lock is not valid' do
462
+ it 'returns nil' do
463
+ lock_info = lock_manager.lock(resource_key, ttl)
464
+ lock_manager.unlock(lock_info)
465
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
466
+ expect(remaining_ttl).to be_nil
467
+ end
468
+ end
469
+
470
+ context 'when server goes away' do
471
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
472
+
473
+ it 'does not raise an error on connection issues' do
474
+ @lock_info = lock_manager.lock(resource_key, ttl)
475
+
476
+ # Replace redis with unreachable instance
477
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
478
+ old_redis = redis_instance.instance_variable_get(:@redis)
479
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
480
+
481
+ expect {
482
+ remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
483
+ expect(remaining_ttl).to be_nil
484
+ }.to_not raise_error
485
+ end
486
+ end
487
+
488
+ context 'when a server comes back' do
489
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
490
+
491
+ it 'recovers from connection issues' do
492
+ @lock_info = lock_manager.lock(resource_key, ttl)
493
+
494
+ # Replace redis with unreachable instance
495
+ redis_instance = lock_manager.instance_variable_get(:@servers).first
496
+ old_redis = redis_instance.instance_variable_get(:@redis)
497
+ redis_instance.instance_variable_set(:@redis, unreachable_redis)
498
+
499
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_nil
500
+
501
+ # Restore redis
502
+ redis_instance.instance_variable_set(:@redis, old_redis)
503
+ expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_truthy
504
+ end
505
+ end
506
+ end
507
+
508
+ describe 'get_remaining_ttl_for_lock' do
509
+ context 'when lock is valid' do
510
+ it 'gets the remaining ttl of a lock' do
511
+ ttl = 20_000
512
+ lock_info = lock_manager.lock(resource_key, ttl)
513
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
514
+ expect(remaining_ttl).to be_within(300).of(ttl)
515
+ lock_manager.unlock(lock_info)
516
+ end
517
+ end
518
+
519
+ context 'when lock is not valid' do
520
+ it 'returns nil' do
521
+ lock_info = lock_manager.lock(resource_key, ttl)
522
+ lock_manager.unlock(lock_info)
523
+ remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
524
+ expect(remaining_ttl).to be_nil
525
+ end
526
+ end
527
+ end
528
+
529
+ describe 'locked?' do
530
+ context 'when lock is available' do
531
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
532
+
533
+ it 'returns true' do
534
+ @lock_info = lock_manager.lock(resource_key, ttl)
535
+ expect(lock_manager).to be_locked(resource_key)
536
+ end
537
+ end
538
+
539
+ context 'when lock is not available' do
540
+ it 'returns false' do
541
+ lock_info = lock_manager.lock(resource_key, ttl)
542
+ lock_manager.unlock(lock_info)
543
+ expect(lock_manager).not_to be_locked(resource_key)
544
+ end
545
+ end
546
+ end
547
+
548
+ describe 'valid_lock?' do
549
+ context 'when lock is available' do
550
+ after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
551
+
552
+ it 'returns true' do
553
+ @lock_info = lock_manager.lock(resource_key, ttl)
554
+ expect(lock_manager).to be_valid_lock(@lock_info)
555
+ end
556
+ end
557
+
558
+ context 'when lock is not available' do
559
+ it 'returns false' do
560
+ lock_info = lock_manager.lock(resource_key, ttl)
561
+ lock_manager.unlock(lock_info)
562
+ expect(lock_manager).not_to be_valid_lock(lock_info)
563
+ end
564
+ end
565
+ end
566
+
311
567
  describe '#default_time_source' do
312
568
  context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
313
569
  it 'returns a callable using Process.clock_gettime()' do
metadata CHANGED
@@ -1,35 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redlock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leandro Moreira
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-11 00:00:00.000000000 Z
11
+ date: 2023-02-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: redis
14
+ name: redis-client
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.0.0
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '5.0'
19
+ version: '0'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
- version: 3.0.0
30
- - - "<"
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
31
32
  - !ruby/object:Gem::Version
32
- version: '5.0'
33
+ version: '2.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.2'
33
41
  - !ruby/object:Gem::Dependency
34
42
  name: coveralls
35
43
  requirement: !ruby/object:Gem::Requirement
@@ -45,45 +53,65 @@ dependencies:
45
53
  - !ruby/object:Gem::Version
46
54
  version: '0.8'
47
55
  - !ruby/object:Gem::Dependency
48
- name: rake
56
+ name: json
49
57
  requirement: !ruby/object:Gem::Requirement
50
58
  requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.3.0
62
+ - - "~>"
63
+ - !ruby/object:Gem::Version
64
+ version: 2.3.1
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 2.3.0
51
72
  - - "~>"
52
73
  - !ruby/object:Gem::Version
53
- version: '11.1'
74
+ version: 2.3.1
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
54
79
  - - ">="
55
80
  - !ruby/object:Gem::Version
56
81
  version: 11.1.2
82
+ - - "~>"
83
+ - !ruby/object:Gem::Version
84
+ version: '13.0'
57
85
  type: :development
58
86
  prerelease: false
59
87
  version_requirements: !ruby/object:Gem::Requirement
60
88
  requirements:
61
- - - "~>"
62
- - !ruby/object:Gem::Version
63
- version: '11.1'
64
89
  - - ">="
65
90
  - !ruby/object:Gem::Version
66
91
  version: 11.1.2
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '13.0'
67
95
  - !ruby/object:Gem::Dependency
68
96
  name: rspec
69
97
  requirement: !ruby/object:Gem::Requirement
70
98
  requirements:
71
- - - "~>"
72
- - !ruby/object:Gem::Version
73
- version: '3'
74
99
  - - ">="
75
100
  - !ruby/object:Gem::Version
76
101
  version: 3.0.0
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '3'
77
105
  type: :development
78
106
  prerelease: false
79
107
  version_requirements: !ruby/object:Gem::Requirement
80
108
  requirements:
81
- - - "~>"
82
- - !ruby/object:Gem::Version
83
- version: '3'
84
109
  - - ">="
85
110
  - !ruby/object:Gem::Version
86
111
  version: 3.0.0
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '3'
87
115
  description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
88
116
  email:
89
117
  - leandro.ribeiro.moreira@gmail.com
@@ -91,12 +119,11 @@ executables: []
91
119
  extensions: []
92
120
  extra_rdoc_files: []
93
121
  files:
122
+ - ".github/workflows/ci.yml"
94
123
  - ".gitignore"
95
124
  - ".rspec"
96
- - ".travis.yml"
97
125
  - CONTRIBUTORS
98
126
  - Gemfile
99
- - Gemfile.lock
100
127
  - LICENSE
101
128
  - Makefile
102
129
  - README.md
@@ -105,6 +132,7 @@ files:
105
132
  - docker-compose.yml
106
133
  - lib/redlock.rb
107
134
  - lib/redlock/client.rb
135
+ - lib/redlock/scripts.rb
108
136
  - lib/redlock/testing.rb
109
137
  - lib/redlock/version.rb
110
138
  - redlock.gemspec
@@ -130,8 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
158
  - !ruby/object:Gem::Version
131
159
  version: '0'
132
160
  requirements: []
133
- rubyforge_project:
134
- rubygems_version: 2.5.2.3
161
+ rubygems_version: 3.0.3.1
135
162
  signing_key:
136
163
  specification_version: 4
137
164
  summary: Distributed lock using Redis written in Ruby.
data/.travis.yml DELETED
@@ -1,8 +0,0 @@
1
- language: ruby
2
- services:
3
- - redis-server
4
- rvm:
5
- - "2.2.2"
6
- script: bundle exec rspec spec
7
- sudo: false
8
- cache: bundler
data/Gemfile.lock DELETED
@@ -1,54 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- redlock (1.0.0)
5
- redis (>= 3.0.0, < 5.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- coveralls (0.8.22)
11
- json (>= 1.8, < 3)
12
- simplecov (~> 0.16.1)
13
- term-ansicolor (~> 1.3)
14
- thor (~> 0.19.4)
15
- 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)
28
- diff-lcs (>= 1.2.0, < 2.0)
29
- rspec-support (~> 3.5.0)
30
- rspec-mocks (3.5.0)
31
- diff-lcs (>= 1.2.0, < 2.0)
32
- rspec-support (~> 3.5.0)
33
- rspec-support (3.5.0)
34
- simplecov (0.16.1)
35
- docile (~> 1.1)
36
- json (>= 1.8, < 3)
37
- simplecov-html (~> 0.10.0)
38
- simplecov-html (0.10.2)
39
- term-ansicolor (1.6.0)
40
- tins (~> 1.0)
41
- thor (0.19.4)
42
- tins (1.16.3)
43
-
44
- PLATFORMS
45
- ruby
46
-
47
- DEPENDENCIES
48
- coveralls (~> 0.8)
49
- rake (~> 11.1, >= 11.1.2)
50
- redlock!
51
- rspec (~> 3, >= 3.0.0)
52
-
53
- BUNDLED WITH
54
- 1.17.2