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