redlock 1.2.0 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +31 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +40 -0
- data/README.md +66 -4
- data/docker-compose.yml +6 -0
- data/lib/redlock/client.rb +140 -36
- data/lib/redlock/scripts.rb +34 -0
- data/lib/redlock/testing.rb +3 -1
- data/lib/redlock/version.rb +1 -1
- data/lib/redlock.rb +1 -0
- data/redlock.gemspec +14 -13
- data/spec/client_spec.rb +283 -27
- data/spec/testing_spec.rb +3 -3
- metadata +44 -29
- data/.travis.yml +0 -10
- data/Gemfile.lock +0 -56
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8bdda4faebf87c2d1fb0026a3142497d5ae14092340bce6e35e46f51635e5c6a
|
4
|
+
data.tar.gz: a9f5636898a2a3ece98b95a286bd88f417e68d119b1f1ad60bd75b5d1da94baf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b10ec10f00ce9282d604b1d0c8a2e845d8d95a6684a5a235d6d39b598efa6324725c4d088e8016b11d473b0195a1e29a451e79fad115f04f6b217493be297cf
|
7
|
+
data.tar.gz: e1d54bb8bb8a108e0989d1c11baa44cbfc4c971029fb23819d000758b42739698342ef803a826990757c888d113fb8a49663df1f3812603965c5498361596f86
|
@@ -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
data/CHANGELOG.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
# Change Log
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
6
|
+
and this project adheres to [Semantic Versioning](http://semver.org/).
|
7
|
+
|
8
|
+
## [Unreleased] - yyyy-mm-dd
|
9
|
+
|
10
|
+
Here we write upgrading notes for brands. It's a team effort to make them as
|
11
|
+
straightforward as possible.
|
12
|
+
|
13
|
+
### Added
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
|
17
|
+
### Fixed
|
18
|
+
|
19
|
+
|
20
|
+
## [2.0.1] - 2023-02-14
|
21
|
+
|
22
|
+
### Added
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
|
28
|
+
* always treat redis instance as pool-like #125
|
29
|
+
|
30
|
+
## [2.0.0] - 2023-02-09
|
31
|
+
|
32
|
+
### Added
|
33
|
+
|
34
|
+
* support for redis >= 6.0
|
35
|
+
|
36
|
+
### Changed
|
37
|
+
|
38
|
+
* **BREAKING**: The library now only works with `RedisClient` instance.
|
39
|
+
|
40
|
+
### Fixed
|
data/README.md
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
[![Build Status](https://
|
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)
|
2
2
|
[![Coverage Status](https://coveralls.io/repos/leandromoreira/redlock-rb/badge.svg?branch=master)](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
|
3
3
|
[![Code Climate](https://codeclimate.com/github/leandromoreira/redlock-rb/badges/gpa.svg)](https://codeclimate.com/github/leandromoreira/redlock-rb)
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
|
5
|
-
[![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
|
6
5
|
[![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
|
7
6
|
|
8
7
|
|
@@ -16,7 +15,8 @@ This is an implementation of a proposed [distributed lock algorithm with Redis](
|
|
16
15
|
|
17
16
|
## Compatibility
|
18
17
|
|
19
|
-
|
18
|
+
* It works with Redis server versions 6.0 or later.
|
19
|
+
* Redlock >= 2.0 only works with [`RedisClient`](https://github.com/redis-rb/redis-client) client instance.
|
20
20
|
|
21
21
|
## Installation
|
22
22
|
|
@@ -112,12 +112,74 @@ The above code will also acquire the lock if the previous lock has expired and t
|
|
112
112
|
lock_manager.lock("resource key", 3000, extend: lock_info, extend_only_if_locked: true)
|
113
113
|
```
|
114
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
|
175
|
+
```
|
176
|
+
|
115
177
|
## Redis client configuration
|
116
178
|
|
117
179
|
`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.
|
118
180
|
|
119
181
|
```ruby
|
120
|
-
servers = [ 'redis://localhost:6379',
|
182
|
+
servers = [ 'redis://localhost:6379', RedisClient.new(:url => 'redis://someotherhost:6379') ]
|
121
183
|
redlock = Redlock::Client.new(servers)
|
122
184
|
```
|
123
185
|
|
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
|
|
data/lib/redlock/client.rb
CHANGED
@@ -1,7 +1,18 @@
|
|
1
|
-
require 'redis'
|
1
|
+
require 'redis-client'
|
2
2
|
require 'securerandom'
|
3
3
|
|
4
4
|
module Redlock
|
5
|
+
include Scripts
|
6
|
+
|
7
|
+
class LockAcquisitionError < StandardError
|
8
|
+
attr_reader :errors
|
9
|
+
|
10
|
+
def initialize(message, errors)
|
11
|
+
super(message)
|
12
|
+
@errors = errors
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
5
16
|
class Client
|
6
17
|
DEFAULT_REDIS_HOST = ENV["DEFAULT_REDIS_HOST"] || "localhost"
|
7
18
|
DEFAULT_REDIS_PORT = ENV["DEFAULT_REDIS_PORT"] || "6379"
|
@@ -54,6 +65,9 @@ module Redlock
|
|
54
65
|
# +resource+:: the resource (or key) string to be locked.
|
55
66
|
# +ttl+:: The time-to-live in ms for the lock.
|
56
67
|
# +options+:: Hash of optional parameters
|
68
|
+
# * +retry_count+: see +initialize+
|
69
|
+
# * +retry_delay+: see +initialize+
|
70
|
+
# * +retry_jitter+: see +initialize+
|
57
71
|
# * +extend+: A lock ("lock_info") to extend.
|
58
72
|
# * +extend_only_if_locked+: Boolean, if +extend+ is given, only acquire lock if currently held
|
59
73
|
# * +extend_only_if_life+: Deprecated, same as +extend_only_if_locked+
|
@@ -102,26 +116,46 @@ module Redlock
|
|
102
116
|
end
|
103
117
|
end
|
104
118
|
|
119
|
+
# Gets remaining ttl of a resource. The ttl is returned if the holder
|
120
|
+
# currently holds the lock and it has not expired, otherwise the method
|
121
|
+
# returns nil.
|
122
|
+
# Params:
|
123
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource
|
124
|
+
def get_remaining_ttl_for_lock(lock_info)
|
125
|
+
ttl_info = try_get_remaining_ttl(lock_info[:resource])
|
126
|
+
return nil if ttl_info.nil? || ttl_info[:value] != lock_info[:value]
|
127
|
+
ttl_info[:ttl]
|
128
|
+
end
|
129
|
+
|
130
|
+
# Gets remaining ttl of a resource. If there is no valid lock, the method
|
131
|
+
# returns nil.
|
132
|
+
# Params:
|
133
|
+
# +resource+:: the name of the resource (string) for which to check the ttl
|
134
|
+
def get_remaining_ttl_for_resource(resource)
|
135
|
+
ttl_info = try_get_remaining_ttl(resource)
|
136
|
+
return nil if ttl_info.nil?
|
137
|
+
ttl_info[:ttl]
|
138
|
+
end
|
139
|
+
|
140
|
+
# Checks if a resource is locked
|
141
|
+
# Params:
|
142
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource
|
143
|
+
def locked?(resource)
|
144
|
+
ttl = get_remaining_ttl_for_resource(resource)
|
145
|
+
!(ttl.nil? || ttl.zero?)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Checks if a lock is still valid
|
149
|
+
# Params:
|
150
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource
|
151
|
+
def valid_lock?(lock_info)
|
152
|
+
ttl = get_remaining_ttl_for_lock(lock_info)
|
153
|
+
!(ttl.nil? || ttl.zero?)
|
154
|
+
end
|
155
|
+
|
105
156
|
private
|
106
157
|
|
107
158
|
class RedisInstance
|
108
|
-
UNLOCK_SCRIPT = <<-eos
|
109
|
-
if redis.call("get",KEYS[1]) == ARGV[1] then
|
110
|
-
return redis.call("del",KEYS[1])
|
111
|
-
else
|
112
|
-
return 0
|
113
|
-
end
|
114
|
-
eos
|
115
|
-
|
116
|
-
# thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
|
117
|
-
# also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
|
118
|
-
# and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
|
119
|
-
LOCK_SCRIPT = <<-eos
|
120
|
-
if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
|
121
|
-
return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
|
122
|
-
end
|
123
|
-
eos
|
124
|
-
|
125
159
|
module ConnectionPoolLike
|
126
160
|
def with
|
127
161
|
yield self
|
@@ -135,42 +169,61 @@ module Redlock
|
|
135
169
|
if connection.respond_to?(:client)
|
136
170
|
@redis = connection
|
137
171
|
else
|
138
|
-
@redis =
|
172
|
+
@redis = RedisClient.new(connection)
|
139
173
|
end
|
140
174
|
@redis.extend(ConnectionPoolLike)
|
141
175
|
end
|
142
|
-
|
143
|
-
load_scripts
|
144
176
|
end
|
145
177
|
|
146
178
|
def lock(resource, val, ttl, allow_new_lock)
|
147
179
|
recover_from_script_flush do
|
148
|
-
@redis.with { |conn|
|
180
|
+
@redis.with { |conn|
|
181
|
+
conn.call('EVALSHA', Scripts::LOCK_SCRIPT_SHA, 1, resource, val, ttl, allow_new_lock)
|
182
|
+
}
|
149
183
|
end
|
150
|
-
rescue Redis::BaseConnectionError
|
151
|
-
false
|
152
184
|
end
|
153
185
|
|
154
186
|
def unlock(resource, val)
|
155
187
|
recover_from_script_flush do
|
156
|
-
@redis.with { |conn|
|
188
|
+
@redis.with { |conn|
|
189
|
+
conn.call('EVALSHA', Scripts::UNLOCK_SCRIPT_SHA, 1, resource, val)
|
190
|
+
}
|
157
191
|
end
|
158
192
|
rescue
|
159
193
|
# Nothing to do, unlocking is just a best-effort attempt.
|
160
194
|
end
|
161
195
|
|
196
|
+
def get_remaining_ttl(resource)
|
197
|
+
recover_from_script_flush do
|
198
|
+
@redis.with { |conn|
|
199
|
+
conn.call('EVALSHA', Scripts::PTTL_SCRIPT_SHA, 1, resource)
|
200
|
+
}
|
201
|
+
end
|
202
|
+
rescue RedisClient::ConnectionError
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
162
206
|
private
|
163
207
|
|
164
208
|
def load_scripts
|
165
|
-
|
166
|
-
|
209
|
+
scripts = [
|
210
|
+
Scripts::UNLOCK_SCRIPT,
|
211
|
+
Scripts::LOCK_SCRIPT,
|
212
|
+
Scripts::PTTL_SCRIPT
|
213
|
+
]
|
214
|
+
|
215
|
+
@redis.with do |connnection|
|
216
|
+
scripts.each do |script|
|
217
|
+
connnection.call('SCRIPT', 'LOAD', script)
|
218
|
+
end
|
219
|
+
end
|
167
220
|
end
|
168
221
|
|
169
222
|
def recover_from_script_flush
|
170
223
|
retry_on_noscript = true
|
171
224
|
begin
|
172
225
|
yield
|
173
|
-
rescue
|
226
|
+
rescue RedisClient::CommandError => e
|
174
227
|
# When somebody has flushed the Redis instance's script cache, we might
|
175
228
|
# want to reload our scripts. Only attempt this once, though, to avoid
|
176
229
|
# going into an infinite loop.
|
@@ -186,36 +239,51 @@ module Redlock
|
|
186
239
|
end
|
187
240
|
|
188
241
|
def try_lock_instances(resource, ttl, options)
|
189
|
-
|
242
|
+
retry_count = options[:retry_count] || @retry_count
|
243
|
+
tries = options[:extend] ? 1 : (retry_count + 1)
|
244
|
+
last_error = nil
|
190
245
|
|
191
246
|
tries.times do |attempt_number|
|
192
247
|
# Wait a random delay before retrying.
|
193
|
-
sleep(attempt_retry_delay(attempt_number)) if attempt_number > 0
|
248
|
+
sleep(attempt_retry_delay(attempt_number, options)) if attempt_number > 0
|
194
249
|
|
195
250
|
lock_info = lock_instances(resource, ttl, options)
|
196
251
|
return lock_info if lock_info
|
252
|
+
rescue => e
|
253
|
+
last_error = e
|
197
254
|
end
|
198
255
|
|
256
|
+
raise last_error if last_error
|
257
|
+
|
199
258
|
false
|
200
259
|
end
|
201
260
|
|
202
|
-
def attempt_retry_delay(attempt_number)
|
261
|
+
def attempt_retry_delay(attempt_number, options)
|
262
|
+
retry_delay = options[:retry_delay] || @retry_delay
|
263
|
+
retry_jitter = options[:retry_jitter] || @retry_jitter
|
264
|
+
|
203
265
|
retry_delay =
|
204
|
-
if
|
205
|
-
|
266
|
+
if retry_delay.respond_to?(:call)
|
267
|
+
retry_delay.call(attempt_number)
|
206
268
|
else
|
207
|
-
|
269
|
+
retry_delay
|
208
270
|
end
|
209
271
|
|
210
|
-
(retry_delay + rand(
|
272
|
+
(retry_delay + rand(retry_jitter)).to_f / 1000
|
211
273
|
end
|
212
274
|
|
213
275
|
def lock_instances(resource, ttl, options)
|
214
276
|
value = (options[:extend] || { value: SecureRandom.uuid })[:value]
|
215
277
|
allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
|
278
|
+
errors = []
|
216
279
|
|
217
280
|
locked, time_elapsed = timed do
|
218
|
-
@servers.
|
281
|
+
@servers.count do |s|
|
282
|
+
s.lock(resource, value, ttl, allow_new_lock)
|
283
|
+
rescue => e
|
284
|
+
errors << e
|
285
|
+
false
|
286
|
+
end
|
219
287
|
end
|
220
288
|
|
221
289
|
validity = ttl - time_elapsed - drift(ttl)
|
@@ -224,10 +292,46 @@ module Redlock
|
|
224
292
|
{ validity: validity, resource: resource, value: value }
|
225
293
|
else
|
226
294
|
@servers.each { |s| s.unlock(resource, value) }
|
295
|
+
|
296
|
+
if errors.size >= @quorum
|
297
|
+
raise LockAcquisitionError.new('Too many Redis errors prevented lock acquisition', errors)
|
298
|
+
end
|
299
|
+
|
227
300
|
false
|
228
301
|
end
|
229
302
|
end
|
230
303
|
|
304
|
+
def try_get_remaining_ttl(resource)
|
305
|
+
# Responses from the servers are a 2 tuple of format [lock_value, ttl].
|
306
|
+
# The lock_value is nil if it does not exist. Since servers may have
|
307
|
+
# different lock values, the responses are grouped by the lock_value and
|
308
|
+
# transofrmed into a hash: { lock_value1 => [ttl1, ttl2, ttl3],
|
309
|
+
# lock_value2 => [ttl4, tt5] }
|
310
|
+
ttls_by_value, time_elapsed = timed do
|
311
|
+
@servers.map { |s| s.get_remaining_ttl(resource) }
|
312
|
+
.select { |ttl_tuple| ttl_tuple&.first }
|
313
|
+
.group_by(&:first)
|
314
|
+
.transform_values { |ttl_tuples| ttl_tuples.map { |t| t.last } }
|
315
|
+
end
|
316
|
+
|
317
|
+
# Authoritative lock value is that which is returned by the majority of
|
318
|
+
# servers
|
319
|
+
authoritative_value, ttls =
|
320
|
+
ttls_by_value.max_by { |(lock_value, ttls)| ttls.length }
|
321
|
+
|
322
|
+
if ttls && ttls.size >= @quorum
|
323
|
+
# Return the minimum TTL of an N/2+1 selection. It will always be
|
324
|
+
# correct (it will guarantee that at least N/2+1 servers have a TTL that
|
325
|
+
# value or longer)
|
326
|
+
min_ttl = ttls.sort.last(@quorum).first
|
327
|
+
min_ttl = min_ttl - time_elapsed - drift(min_ttl)
|
328
|
+
{ value: authoritative_value, ttl: min_ttl }
|
329
|
+
else
|
330
|
+
# No lock_value is authoritatively held for the resource
|
331
|
+
nil
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
231
335
|
def drift(ttl)
|
232
336
|
# Add 2 milliseconds to the drift to account for Redis expires
|
233
337
|
# 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
|
data/lib/redlock/testing.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'redlock'
|
2
|
+
|
1
3
|
module Redlock
|
2
4
|
class Client
|
3
5
|
class << self
|
@@ -39,7 +41,7 @@ module Redlock
|
|
39
41
|
|
40
42
|
def load_scripts
|
41
43
|
load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
|
42
|
-
rescue
|
44
|
+
rescue RedisClient::CommandError
|
43
45
|
# FakeRedis doesn't have #script, but doesn't need it either.
|
44
46
|
raise unless defined?(::FakeRedis)
|
45
47
|
rescue NoMethodError
|
data/lib/redlock/version.rb
CHANGED
data/lib/redlock.rb
CHANGED
data/redlock.gemspec
CHANGED
@@ -1,27 +1,28 @@
|
|
1
|
-
#
|
2
|
-
|
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 =
|
8
|
+
spec.name = 'redlock'
|
8
9
|
spec.version = Redlock::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
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 = [
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_dependency 'redis', '
|
21
|
+
spec.add_dependency 'redis-client', '~> 0.14.1'
|
22
22
|
|
23
|
-
spec.add_development_dependency
|
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'
|
24
26
|
spec.add_development_dependency 'rake', '>= 11.1.2', '~> 13.0'
|
25
27
|
spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
|
26
|
-
spec.add_development_dependency 'connection_pool', '~> 2.2'
|
27
28
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,46 +1,113 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'securerandom'
|
3
|
-
require 'redis'
|
4
3
|
require 'connection_pool'
|
5
4
|
|
6
5
|
RSpec.describe Redlock::Client do
|
7
6
|
# It is recommended to have at least 3 servers in production
|
8
7
|
let(:lock_manager_opts) { { retry_count: 3 } }
|
9
|
-
let(:
|
10
|
-
|
8
|
+
let(:redis_urls_or_clients) {
|
9
|
+
urls = Redlock::Client::DEFAULT_REDIS_URLS
|
10
|
+
if rand(0..1).zero?
|
11
|
+
RSpec.configuration.reporter.message "variant: client urls"
|
12
|
+
urls
|
13
|
+
else
|
14
|
+
RSpec.configuration.reporter.message "variant: client objects"
|
15
|
+
urls.map {|url|
|
16
|
+
ConnectionPool.new { RedisClient.new(url: url) }
|
17
|
+
}
|
18
|
+
end
|
19
|
+
}
|
20
|
+
let(:lock_manager) {
|
21
|
+
Redlock::Client.new(redis_urls_or_clients, lock_manager_opts)
|
22
|
+
}
|
23
|
+
let(:redis_client) { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
11
24
|
let(:resource_key) { SecureRandom.hex(3) }
|
12
25
|
let(:ttl) { 1000 }
|
13
26
|
let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
|
14
27
|
let(:redis1_port) { ENV["REDIS1_PORT"] || "6379" }
|
15
28
|
let(:redis2_host) { ENV["REDIS2_HOST"] || "127.0.0.1" }
|
16
29
|
let(:redis2_port) { ENV["REDIS2_PORT"] || "6379" }
|
30
|
+
let(:redis3_host) { ENV["REDIS3_HOST"] || "127.0.0.1" }
|
31
|
+
let(:redis3_port) { ENV["REDIS3_PORT"] || "6379" }
|
32
|
+
let(:unreachable_redis) {
|
33
|
+
redis = RedisClient.new(url: 'redis://localhost:46864')
|
34
|
+
def redis.with
|
35
|
+
yield self
|
36
|
+
end
|
37
|
+
redis
|
38
|
+
}
|
17
39
|
|
18
40
|
describe 'initialize' do
|
19
41
|
it 'accepts both redis URLs and Redis objects' do
|
20
|
-
servers = [ "redis://#{redis1_host}:#{redis1_port}",
|
42
|
+
servers = [ "redis://#{redis1_host}:#{redis1_port}", RedisClient.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
|
21
43
|
redlock = Redlock::Client.new(servers)
|
22
44
|
|
23
45
|
redlock_servers = redlock.instance_variable_get(:@servers).map do |s|
|
24
|
-
s.instance_variable_get(:@redis).
|
46
|
+
s.instance_variable_get(:@redis).config.host
|
25
47
|
end
|
26
48
|
|
27
49
|
expect(redlock_servers).to match_array([redis1_host, redis2_host])
|
28
50
|
end
|
29
51
|
|
30
52
|
it 'accepts ConnectionPool objects' do
|
31
|
-
pool = ConnectionPool.new {
|
32
|
-
|
53
|
+
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
54
|
+
_redlock = Redlock::Client.new([pool])
|
33
55
|
|
34
56
|
lock_info = lock_manager.lock(resource_key, ttl)
|
57
|
+
expect(lock_info).to be_a(Hash)
|
35
58
|
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
36
59
|
lock_manager.unlock(lock_info)
|
37
60
|
end
|
61
|
+
|
62
|
+
it 'does not load scripts' do
|
63
|
+
redis_client.call('SCRIPT', 'FLUSH')
|
64
|
+
|
65
|
+
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
66
|
+
_redlock = Redlock::Client.new([pool])
|
67
|
+
|
68
|
+
raw_info = redis_client.call('INFO')
|
69
|
+
number_of_cached_scripts = raw_info[/number_of_cached_scripts\:\d+/].split(':').last
|
70
|
+
|
71
|
+
expect(number_of_cached_scripts).to eq("0")
|
72
|
+
end
|
38
73
|
end
|
39
74
|
|
40
75
|
describe 'lock' do
|
41
76
|
context 'when lock is available' do
|
42
77
|
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
43
78
|
|
79
|
+
context 'when redis connection error occurs' do
|
80
|
+
let(:servers_with_quorum) {
|
81
|
+
[
|
82
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
83
|
+
"redis://#{redis2_host}:#{redis2_port}",
|
84
|
+
unreachable_redis
|
85
|
+
]
|
86
|
+
}
|
87
|
+
|
88
|
+
let(:servers_without_quorum) {
|
89
|
+
[
|
90
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
91
|
+
unreachable_redis,
|
92
|
+
unreachable_redis
|
93
|
+
]
|
94
|
+
}
|
95
|
+
|
96
|
+
it 'locks if majority of redis instances are available' do
|
97
|
+
redlock = Redlock::Client.new(servers_with_quorum)
|
98
|
+
|
99
|
+
expect(redlock.lock(resource_key, ttl)).to be_truthy
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'fails to acquire a lock if majority of Redis instances are not available' do
|
103
|
+
redlock = Redlock::Client.new(servers_without_quorum)
|
104
|
+
|
105
|
+
expect {
|
106
|
+
redlock.lock(resource_key, ttl)
|
107
|
+
}.to raise_error(Redlock::LockAcquisitionError)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
44
111
|
it 'locks' do
|
45
112
|
@lock_info = lock_manager.lock(resource_key, ttl)
|
46
113
|
|
@@ -56,7 +123,7 @@ RSpec.describe Redlock::Client do
|
|
56
123
|
it 'interprets lock time as milliseconds' do
|
57
124
|
ttl = 20000
|
58
125
|
@lock_info = lock_manager.lock(resource_key, ttl)
|
59
|
-
expect(redis_client.
|
126
|
+
expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
|
60
127
|
end
|
61
128
|
|
62
129
|
it 'can extend its own lock' do
|
@@ -88,7 +155,7 @@ RSpec.describe Redlock::Client do
|
|
88
155
|
|
89
156
|
lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
|
90
157
|
expect(lock_info).not_to be_nil
|
91
|
-
expect(redis_client.
|
158
|
+
expect(redis_client.call('PTTL', resource_key)).to be_within(200).of(ttl)
|
92
159
|
end
|
93
160
|
|
94
161
|
context 'when extend_only_if_locked flag is not given' do
|
@@ -177,7 +244,7 @@ RSpec.describe Redlock::Client do
|
|
177
244
|
2000
|
178
245
|
end
|
179
246
|
|
180
|
-
lock_manager = Redlock::Client.new(
|
247
|
+
lock_manager = Redlock::Client.new(redis_urls_or_clients, retry_count: 1, retry_delay: retry_delay)
|
181
248
|
another_lock_info = lock_manager.lock(resource_key, ttl)
|
182
249
|
|
183
250
|
expect(lock_manager).to receive(:sleep) do |sleep|
|
@@ -186,17 +253,55 @@ RSpec.describe Redlock::Client do
|
|
186
253
|
lock_manager.lock(resource_key, ttl)
|
187
254
|
lock_manager.unlock(another_lock_info)
|
188
255
|
end
|
256
|
+
|
257
|
+
context 'when retry_count is given' do
|
258
|
+
it 'prioritizes the retry_count in option and tries up to \'retry_count\' + 1 times' do
|
259
|
+
retry_count = 1
|
260
|
+
expect(retry_count).not_to eq(lock_manager_opts[:retry_count])
|
261
|
+
expect(lock_manager).to receive(:lock_instances).exactly(retry_count + 1).times.and_return(false)
|
262
|
+
lock_manager.lock(resource_key, ttl, retry_count: retry_count)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context 'when retry_delay is given' do
|
267
|
+
it 'prioritizes the retry_delay in option and sleeps at least the specified retry_delay in milliseconds' do
|
268
|
+
retry_delay = 300
|
269
|
+
expect(retry_delay > described_class::DEFAULT_RETRY_DELAY).to eq(true)
|
270
|
+
expected_minimum = retry_delay
|
271
|
+
|
272
|
+
expect(lock_manager).to receive(:sleep) do |sleep|
|
273
|
+
expect(sleep).to satisfy { |value| value >= expected_minimum / 1000.to_f }
|
274
|
+
end.at_least(:once)
|
275
|
+
lock_manager.lock(resource_key, ttl, retry_delay: retry_delay)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
context 'when retry_jitter is given' do
|
280
|
+
it 'prioritizes the retry_jitter in option and sleeps a maximum of retry_delay + retry_jitter in milliseconds' do
|
281
|
+
retry_jitter = 60
|
282
|
+
expect(retry_jitter > described_class::DEFAULT_RETRY_JITTER).to eq(true)
|
283
|
+
|
284
|
+
expected_maximum = described_class::DEFAULT_RETRY_DELAY + retry_jitter
|
285
|
+
expect(lock_manager).to receive(:sleep) do |sleep|
|
286
|
+
expect(sleep).to satisfy { |value| value < expected_maximum / 1000.to_f }
|
287
|
+
end.at_least(:once)
|
288
|
+
lock_manager.lock(resource_key, ttl, retry_jitter: retry_jitter)
|
289
|
+
end
|
290
|
+
end
|
189
291
|
end
|
190
292
|
|
191
293
|
context 'when a server goes away' do
|
192
|
-
it '
|
193
|
-
#
|
294
|
+
it 'raises an error on connection issues' do
|
295
|
+
# Set lock manager to a (hopefully) non-existent Redis URL to test error
|
194
296
|
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
195
297
|
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
196
298
|
|
197
299
|
expect {
|
198
|
-
|
199
|
-
}.
|
300
|
+
lock_manager.lock(resource_key, ttl)
|
301
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
302
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
303
|
+
expect(e.errors.count).to eq 1
|
304
|
+
end
|
200
305
|
end
|
201
306
|
end
|
202
307
|
|
@@ -206,27 +311,26 @@ RSpec.describe Redlock::Client do
|
|
206
311
|
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
207
312
|
old_redis = redis_instance.instance_variable_get(:@redis)
|
208
313
|
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
209
|
-
expect
|
314
|
+
expect {
|
315
|
+
lock_manager.lock(resource_key, ttl)
|
316
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
317
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
318
|
+
expect(e.errors.count).to eq 1
|
319
|
+
end
|
210
320
|
redis_instance.instance_variable_set(:@redis, old_redis)
|
211
321
|
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
|
212
322
|
end
|
213
323
|
end
|
214
324
|
|
215
|
-
def unreachable_redis
|
216
|
-
redis = Redis.new(url: 'redis://localhost:46864')
|
217
|
-
def redis.with
|
218
|
-
yield self
|
219
|
-
end
|
220
|
-
redis
|
221
|
-
end
|
222
|
-
|
223
325
|
context 'when script cache has been flushed' do
|
224
326
|
before(:each) do
|
225
327
|
@manipulated_instance = lock_manager.instance_variable_get(:@servers).first
|
226
|
-
@manipulated_instance.instance_variable_get(:@redis).
|
328
|
+
@manipulated_instance.instance_variable_get(:@redis).with { |conn|
|
329
|
+
conn.call('SCRIPT', 'FLUSH')
|
330
|
+
}
|
227
331
|
end
|
228
332
|
|
229
|
-
it 'does not raise a
|
333
|
+
it 'does not raise a RedisClient::CommandError: NOSCRIPT error' do
|
230
334
|
expect {
|
231
335
|
lock_manager.lock(resource_key, ttl)
|
232
336
|
}.to_not raise_error
|
@@ -242,11 +346,15 @@ RSpec.describe Redlock::Client do
|
|
242
346
|
# This time we do not pass it through to Redis, in order to simulate a passing
|
243
347
|
# call to LOAD SCRIPT followed by another NOSCRIPT error. Imagine someone
|
244
348
|
# repeatedly calling SCRIPT FLUSH on our Redis instance.
|
245
|
-
expect(@manipulated_instance).to receive(:load_scripts)
|
349
|
+
expect(@manipulated_instance).to receive(:load_scripts).exactly(8).times
|
246
350
|
|
247
351
|
expect {
|
248
352
|
lock_manager.lock(resource_key, ttl)
|
249
|
-
}.to raise_error(
|
353
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
354
|
+
expect(e.errors[0]).to be_a(RedisClient::CommandError)
|
355
|
+
expect(e.errors[0].message).to match(/NOSCRIPT/)
|
356
|
+
expect(e.errors.count).to eq 1
|
357
|
+
end
|
250
358
|
end
|
251
359
|
end
|
252
360
|
|
@@ -367,6 +475,154 @@ RSpec.describe Redlock::Client do
|
|
367
475
|
end
|
368
476
|
end
|
369
477
|
|
478
|
+
describe 'get_remaining_ttl_for_resource' do
|
479
|
+
context 'when lock is valid' do
|
480
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
481
|
+
|
482
|
+
it 'gets the remaining ttl of a lock' do
|
483
|
+
ttl = 20_000
|
484
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
485
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
|
486
|
+
expect(remaining_ttl).to be_within(300).of(ttl)
|
487
|
+
end
|
488
|
+
|
489
|
+
context 'when servers respond with varying ttls' do
|
490
|
+
let (:servers) {
|
491
|
+
[
|
492
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
493
|
+
"redis://#{redis2_host}:#{redis2_port}",
|
494
|
+
"redis://#{redis3_host}:#{redis3_port}"
|
495
|
+
]
|
496
|
+
}
|
497
|
+
let (:redlock) { Redlock::Client.new(servers) }
|
498
|
+
after(:each) { redlock.unlock(@lock_info) if @lock_info }
|
499
|
+
|
500
|
+
it 'returns the minimum ttl value' do
|
501
|
+
ttl = 20_000
|
502
|
+
@lock_info = redlock.lock(resource_key, ttl)
|
503
|
+
|
504
|
+
# Mock redis server responses to return different ttls
|
505
|
+
returned_ttls = [20_000, 15_000, 10_000]
|
506
|
+
redlock.instance_variable_get(:@servers).each_with_index do |server, index|
|
507
|
+
allow(server).to(receive(:get_remaining_ttl))
|
508
|
+
.with(resource_key)
|
509
|
+
.and_return([@lock_info[:value], returned_ttls[index]])
|
510
|
+
end
|
511
|
+
|
512
|
+
remaining_ttl = redlock.get_remaining_ttl_for_lock(@lock_info)
|
513
|
+
|
514
|
+
# Assert that the TTL is closest to the closest to the correct value
|
515
|
+
expect(remaining_ttl).to be_within(300).of(returned_ttls[1])
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
context 'when lock is not valid' do
|
521
|
+
it 'returns nil' do
|
522
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
523
|
+
lock_manager.unlock(lock_info)
|
524
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
|
525
|
+
expect(remaining_ttl).to be_nil
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
context 'when server goes away' do
|
530
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
531
|
+
|
532
|
+
it 'does not raise an error on connection issues' do
|
533
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
534
|
+
|
535
|
+
# Replace redis with unreachable instance
|
536
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
537
|
+
_old_redis = redis_instance.instance_variable_get(:@redis)
|
538
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
539
|
+
|
540
|
+
expect {
|
541
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
|
542
|
+
expect(remaining_ttl).to be_nil
|
543
|
+
}.to_not raise_error
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
context 'when a server comes back' do
|
548
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
549
|
+
|
550
|
+
it 'recovers from connection issues' do
|
551
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
552
|
+
|
553
|
+
# Replace redis with unreachable instance
|
554
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
555
|
+
old_redis = redis_instance.instance_variable_get(:@redis)
|
556
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
557
|
+
|
558
|
+
expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_nil
|
559
|
+
|
560
|
+
# Restore redis
|
561
|
+
redis_instance.instance_variable_set(:@redis, old_redis)
|
562
|
+
expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_truthy
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
describe 'get_remaining_ttl_for_lock' do
|
568
|
+
context 'when lock is valid' do
|
569
|
+
it 'gets the remaining ttl of a lock' do
|
570
|
+
ttl = 20_000
|
571
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
572
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
|
573
|
+
expect(remaining_ttl).to be_within(300).of(ttl)
|
574
|
+
lock_manager.unlock(lock_info)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
context 'when lock is not valid' do
|
579
|
+
it 'returns nil' do
|
580
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
581
|
+
lock_manager.unlock(lock_info)
|
582
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
|
583
|
+
expect(remaining_ttl).to be_nil
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
describe 'locked?' do
|
589
|
+
context 'when lock is available' do
|
590
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
591
|
+
|
592
|
+
it 'returns true' do
|
593
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
594
|
+
expect(lock_manager).to be_locked(resource_key)
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
context 'when lock is not available' do
|
599
|
+
it 'returns false' do
|
600
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
601
|
+
lock_manager.unlock(lock_info)
|
602
|
+
expect(lock_manager).not_to be_locked(resource_key)
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
|
607
|
+
describe 'valid_lock?' do
|
608
|
+
context 'when lock is available' do
|
609
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
610
|
+
|
611
|
+
it 'returns true' do
|
612
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
613
|
+
expect(lock_manager).to be_valid_lock(@lock_info)
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
context 'when lock is not available' do
|
618
|
+
it 'returns false' do
|
619
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
620
|
+
lock_manager.unlock(lock_info)
|
621
|
+
expect(lock_manager).not_to be_valid_lock(lock_info)
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
370
626
|
describe '#default_time_source' do
|
371
627
|
context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
|
372
628
|
it 'returns a callable using Process.clock_gettime()' do
|
data/spec/testing_spec.rb
CHANGED
@@ -11,7 +11,7 @@ RSpec.describe Redlock::Client do
|
|
11
11
|
describe '(testing mode)' do
|
12
12
|
describe 'try_lock_instances' do
|
13
13
|
context 'when testing with bypass mode' do
|
14
|
-
before {
|
14
|
+
before { Redlock::Client.testing_mode = :bypass }
|
15
15
|
|
16
16
|
it 'bypasses the redis servers' do
|
17
17
|
expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
|
@@ -22,7 +22,7 @@ RSpec.describe Redlock::Client do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
context 'when testing with fail mode' do
|
25
|
-
before {
|
25
|
+
before { Redlock::Client.testing_mode = :fail }
|
26
26
|
|
27
27
|
it 'fails' do
|
28
28
|
expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
|
@@ -33,7 +33,7 @@ RSpec.describe Redlock::Client do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
context 'when testing is disabled' do
|
36
|
-
before {
|
36
|
+
before { Redlock::Client.testing_mode = nil }
|
37
37
|
|
38
38
|
it 'works as usual' do
|
39
39
|
expect(lock_manager).to receive(:try_lock_instances_without_testing)
|
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:
|
4
|
+
version: 2.0.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:
|
11
|
+
date: 2023-06-15 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
|
-
- - "
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 3.0.0
|
20
|
-
- - "<"
|
17
|
+
- - "~>"
|
21
18
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
19
|
+
version: 0.14.1
|
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:
|
30
|
-
|
26
|
+
version: 0.14.1
|
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: '
|
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
|
@@ -44,6 +52,26 @@ dependencies:
|
|
44
52
|
- - "~>"
|
45
53
|
- !ruby/object:Gem::Version
|
46
54
|
version: '0.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: json
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
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
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.3.1
|
47
75
|
- !ruby/object:Gem::Dependency
|
48
76
|
name: rake
|
49
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -84,20 +112,6 @@ dependencies:
|
|
84
112
|
- - ">="
|
85
113
|
- !ruby/object:Gem::Version
|
86
114
|
version: 3.0.0
|
87
|
-
- !ruby/object:Gem::Dependency
|
88
|
-
name: connection_pool
|
89
|
-
requirement: !ruby/object:Gem::Requirement
|
90
|
-
requirements:
|
91
|
-
- - "~>"
|
92
|
-
- !ruby/object:Gem::Version
|
93
|
-
version: '2.2'
|
94
|
-
type: :development
|
95
|
-
prerelease: false
|
96
|
-
version_requirements: !ruby/object:Gem::Requirement
|
97
|
-
requirements:
|
98
|
-
- - "~>"
|
99
|
-
- !ruby/object:Gem::Version
|
100
|
-
version: '2.2'
|
101
115
|
description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
|
102
116
|
email:
|
103
117
|
- leandro.ribeiro.moreira@gmail.com
|
@@ -105,12 +119,12 @@ executables: []
|
|
105
119
|
extensions: []
|
106
120
|
extra_rdoc_files: []
|
107
121
|
files:
|
122
|
+
- ".github/workflows/ci.yml"
|
108
123
|
- ".gitignore"
|
109
124
|
- ".rspec"
|
110
|
-
-
|
125
|
+
- CHANGELOG.md
|
111
126
|
- CONTRIBUTORS
|
112
127
|
- Gemfile
|
113
|
-
- Gemfile.lock
|
114
128
|
- LICENSE
|
115
129
|
- Makefile
|
116
130
|
- README.md
|
@@ -119,6 +133,7 @@ files:
|
|
119
133
|
- docker-compose.yml
|
120
134
|
- lib/redlock.rb
|
121
135
|
- lib/redlock/client.rb
|
136
|
+
- lib/redlock/scripts.rb
|
122
137
|
- lib/redlock/testing.rb
|
123
138
|
- lib/redlock/version.rb
|
124
139
|
- redlock.gemspec
|
@@ -144,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
159
|
- !ruby/object:Gem::Version
|
145
160
|
version: '0'
|
146
161
|
requirements: []
|
147
|
-
rubygems_version: 3.
|
162
|
+
rubygems_version: 3.3.7
|
148
163
|
signing_key:
|
149
164
|
specification_version: 4
|
150
165
|
summary: Distributed lock using Redis written in Ruby.
|
data/.travis.yml
DELETED
data/Gemfile.lock
DELETED
@@ -1,56 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
redlock (1.2.0)
|
5
|
-
redis (>= 3.0.0, < 5.0)
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
connection_pool (2.2.2)
|
11
|
-
coveralls (0.8.22)
|
12
|
-
json (>= 1.8, < 3)
|
13
|
-
simplecov (~> 0.16.1)
|
14
|
-
term-ansicolor (~> 1.3)
|
15
|
-
thor (~> 0.19.4)
|
16
|
-
tins (~> 1.6)
|
17
|
-
diff-lcs (1.3)
|
18
|
-
docile (1.3.1)
|
19
|
-
json (2.1.0)
|
20
|
-
rake (13.0.1)
|
21
|
-
redis (4.1.1)
|
22
|
-
rspec (3.5.0)
|
23
|
-
rspec-core (~> 3.5.0)
|
24
|
-
rspec-expectations (~> 3.5.0)
|
25
|
-
rspec-mocks (~> 3.5.0)
|
26
|
-
rspec-core (3.5.4)
|
27
|
-
rspec-support (~> 3.5.0)
|
28
|
-
rspec-expectations (3.5.0)
|
29
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
30
|
-
rspec-support (~> 3.5.0)
|
31
|
-
rspec-mocks (3.5.0)
|
32
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
-
rspec-support (~> 3.5.0)
|
34
|
-
rspec-support (3.5.0)
|
35
|
-
simplecov (0.16.1)
|
36
|
-
docile (~> 1.1)
|
37
|
-
json (>= 1.8, < 3)
|
38
|
-
simplecov-html (~> 0.10.0)
|
39
|
-
simplecov-html (0.10.2)
|
40
|
-
term-ansicolor (1.6.0)
|
41
|
-
tins (~> 1.0)
|
42
|
-
thor (0.19.4)
|
43
|
-
tins (1.16.3)
|
44
|
-
|
45
|
-
PLATFORMS
|
46
|
-
ruby
|
47
|
-
|
48
|
-
DEPENDENCIES
|
49
|
-
connection_pool (~> 2.2)
|
50
|
-
coveralls (~> 0.8)
|
51
|
-
rake (~> 13.0, >= 11.1.2)
|
52
|
-
redlock!
|
53
|
-
rspec (~> 3, >= 3.0.0)
|
54
|
-
|
55
|
-
BUNDLED WITH
|
56
|
-
1.17.2
|