redlock 0.2.2 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.travis.yml +20 -5
- data/Makefile +5 -0
- data/README.md +102 -29
- data/docker-compose.yml +6 -0
- data/lib/redlock.rb +5 -1
- data/lib/redlock/client.rb +145 -16
- data/lib/redlock/testing.rb +17 -5
- data/lib/redlock/version.rb +1 -1
- data/redlock.gemspec +14 -11
- data/spec/client_spec.rb +276 -9
- data/spec/spec_helper.rb +8 -0
- metadata +44 -12
- data/Gemfile.lock +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a91b03cbb845e7b01556262e80e946e2d72980beeebce12d51380d7c57c57fb9
|
4
|
+
data.tar.gz: 73d38bac32a8003a556d3d8967155d15a8edbe988fc43387174ec506b21e8c99
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e55aa47e007175e619e22ec2b2ca996add245143290750c79a27fbafc8bb6bd32e5c02b07d9a9752c1ad8cb7f22a10a2b245c9b963c1b4f186427d6c7a987d3a
|
7
|
+
data.tar.gz: dd61e0d187a5972ecbe09bde3345c1a6174263f06fca6b7d1da3ce8be19fe5bb69ffca4cfc24073705c5b5a40551bb1a65ffba1358c5fbaf37f10ea46a15a929
|
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/Makefile
CHANGED
data/README.md
CHANGED
@@ -1,12 +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
|
-
[![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
5
|
[![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
|
8
6
|
[![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
7
|
|
11
8
|
|
12
9
|
# Redlock - A ruby distributed lock using redis.
|
@@ -43,30 +40,28 @@ Or install it yourself as:
|
|
43
40
|
|
44
41
|
## Usage example
|
45
42
|
|
43
|
+
### Acquiring a lock
|
44
|
+
|
45
|
+
NOTE: All expiration durations are in milliseconds.
|
46
46
|
```ruby
|
47
47
|
# Locking
|
48
48
|
lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
|
49
49
|
first_try_lock_info = lock_manager.lock("resource_key", 2000)
|
50
50
|
second_try_lock_info = lock_manager.lock("resource_key", 2000)
|
51
51
|
|
52
|
-
# it prints lock info {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
|
53
52
|
p first_try_lock_info
|
54
|
-
#
|
53
|
+
# => {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
|
54
|
+
|
55
55
|
p second_try_lock_info
|
56
|
+
# => false
|
56
57
|
|
57
58
|
# Unlocking
|
58
59
|
lock_manager.unlock(first_try_lock_info)
|
60
|
+
|
59
61
|
second_try_lock_info = lock_manager.lock("resource_key", 2000)
|
60
62
|
|
61
|
-
# now it prints lock info
|
62
63
|
p second_try_lock_info
|
63
|
-
|
64
|
-
|
65
|
-
Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+. It also allows clients to set any other arbitrary options on the Redis connection, e.g. password, driver, and more.
|
66
|
-
|
67
|
-
```ruby
|
68
|
-
servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
|
69
|
-
redlock = Redlock::Client.new(servers)
|
64
|
+
# => {validity: 1962, resource: "resource_key", value: "generated_uuid5"}
|
70
65
|
```
|
71
66
|
|
72
67
|
There's also a block version that automatically unlocks the lock:
|
@@ -81,7 +76,7 @@ lock_manager.lock("resource_key", 2000) do |locked|
|
|
81
76
|
end
|
82
77
|
```
|
83
78
|
|
84
|
-
There's also a bang version that only executes the block if the lock is successfully acquired, returning the block's value as a result, or raising an exception otherwise
|
79
|
+
There's also a bang version that only executes the block if the lock is successfully acquired, returning the block's value as a result, or raising an exception otherwise. Passing a block is mandatory.
|
85
80
|
|
86
81
|
```ruby
|
87
82
|
begin
|
@@ -93,34 +88,105 @@ rescue Redlock::LockError
|
|
93
88
|
end
|
94
89
|
```
|
95
90
|
|
91
|
+
### Extending a lock
|
92
|
+
|
96
93
|
To extend the life of the lock:
|
97
94
|
|
98
95
|
```ruby
|
99
96
|
begin
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
97
|
+
lock_info = lock_manager.lock("resource_key", 2000)
|
98
|
+
while lock_info
|
99
|
+
# Critical code
|
100
|
+
|
101
|
+
# Time up and more work to do? Extend the lock.
|
102
|
+
lock_info = lock_manager.lock("resource key", 3000, extend: lock_info)
|
104
103
|
end
|
105
104
|
rescue Redlock::LockError
|
106
105
|
# error handling
|
107
106
|
end
|
108
107
|
```
|
109
108
|
|
110
|
-
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 `
|
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:
|
111
110
|
|
112
111
|
```ruby
|
113
|
-
|
114
|
-
block_result = lock_manager.lock!("resource_key", 2000) do |lock_info|
|
115
|
-
# critical code
|
116
|
-
lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
|
117
|
-
# more critical code, only if lock was still hold
|
118
|
-
end
|
119
|
-
rescue Redlock::LockError
|
120
|
-
# error handling
|
121
|
-
end
|
112
|
+
lock_manager.lock("resource key", 3000, extend: lock_info, extend_only_if_locked: true)
|
122
113
|
```
|
123
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
|
+
|
177
|
+
## Redis client configuration
|
178
|
+
|
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.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
|
183
|
+
redlock = Redlock::Client.new(servers)
|
184
|
+
```
|
185
|
+
|
186
|
+
Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+.
|
187
|
+
|
188
|
+
## Redlock configuration
|
189
|
+
|
124
190
|
It's possible to customize the retry logic providing the following options:
|
125
191
|
|
126
192
|
```ruby
|
@@ -133,8 +199,15 @@ It's possible to customize the retry logic providing the following options:
|
|
133
199
|
})
|
134
200
|
```
|
135
201
|
|
136
|
-
|
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
|
+
```
|
137
209
|
|
210
|
+
For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
|
138
211
|
|
139
212
|
## Run tests
|
140
213
|
|
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.rb
CHANGED
data/lib/redlock/client.rb
CHANGED
@@ -12,14 +12,27 @@ module Redlock
|
|
12
12
|
DEFAULT_RETRY_JITTER = 50
|
13
13
|
CLOCK_DRIFT_FACTOR = 0.01
|
14
14
|
|
15
|
+
##
|
16
|
+
# Returns default time source function depending on CLOCK_MONOTONIC availability.
|
17
|
+
#
|
18
|
+
def self.default_time_source
|
19
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
20
|
+
proc { (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i }
|
21
|
+
else
|
22
|
+
proc { (Time.now.to_f * 1000).to_i }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
15
26
|
# Create a distributed lock manager implementing redlock algorithm.
|
16
27
|
# Params:
|
17
28
|
# +servers+:: The array of redis connection URLs or Redis connection instances. Or a mix of both.
|
18
|
-
# +options+::
|
29
|
+
# +options+::
|
19
30
|
# * `retry_count` being how many times it'll try to lock a resource (default: 3)
|
20
31
|
# * `retry_delay` being how many ms to sleep before try to lock again (default: 200)
|
21
32
|
# * `retry_jitter` being how many ms to jitter retry delay (default: 50)
|
22
33
|
# * `redis_timeout` being how the Redis timeout will be set in seconds (default: 0.1)
|
34
|
+
# * `time_source` being a callable object returning a monotonic time in milliseconds
|
35
|
+
# (default: see #default_time_source)
|
23
36
|
def initialize(servers = DEFAULT_REDIS_URLS, options = {})
|
24
37
|
redis_timeout = options[:redis_timeout] || DEFAULT_REDIS_TIMEOUT
|
25
38
|
@servers = servers.map do |server|
|
@@ -33,6 +46,7 @@ module Redlock
|
|
33
46
|
@retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
|
34
47
|
@retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
|
35
48
|
@retry_jitter = options[:retry_jitter] || DEFAULT_RETRY_JITTER
|
49
|
+
@time_source = options[:time_source] || self.class.default_time_source
|
36
50
|
end
|
37
51
|
|
38
52
|
# Locks a resource for a given time.
|
@@ -41,11 +55,21 @@ module Redlock
|
|
41
55
|
# +ttl+:: The time-to-live in ms for the lock.
|
42
56
|
# +options+:: Hash of optional parameters
|
43
57
|
# * +extend+: A lock ("lock_info") to extend.
|
44
|
-
# * +
|
58
|
+
# * +extend_only_if_locked+: Boolean, if +extend+ is given, only acquire lock if currently held
|
59
|
+
# * +extend_only_if_life+: Deprecated, same as +extend_only_if_locked+
|
60
|
+
# * +extend_life+: Deprecated, same as +extend_only_if_locked+
|
45
61
|
# +block+:: an optional block to be executed; after its execution, the lock (if successfully
|
46
62
|
# acquired) is automatically unlocked.
|
47
63
|
def lock(resource, ttl, options = {}, &block)
|
48
64
|
lock_info = try_lock_instances(resource, ttl, options)
|
65
|
+
if options[:extend_only_if_life] && !Gem::Deprecate.skip
|
66
|
+
warn 'DEPRECATION WARNING: The `extend_only_if_life` option has been renamed `extend_only_if_locked`.'
|
67
|
+
options[:extend_only_if_locked] = options[:extend_only_if_life]
|
68
|
+
end
|
69
|
+
if options[:extend_life] && !Gem::Deprecate.skip
|
70
|
+
warn 'DEPRECATION WARNING: The `extend_life` option has been renamed `extend_only_if_locked`.'
|
71
|
+
options[:extend_only_if_locked] = options[:extend_life]
|
72
|
+
end
|
49
73
|
|
50
74
|
if block_given?
|
51
75
|
begin
|
@@ -69,15 +93,52 @@ module Redlock
|
|
69
93
|
# Locks a resource, executing the received block only after successfully acquiring the lock,
|
70
94
|
# and returning its return value as a result.
|
71
95
|
# See Redlock::Client#lock for parameters.
|
72
|
-
def lock!(*args)
|
96
|
+
def lock!(resource, *args)
|
73
97
|
fail 'No block passed' unless block_given?
|
74
98
|
|
75
|
-
lock(*args) do |lock_info|
|
76
|
-
raise LockError,
|
99
|
+
lock(resource, *args) do |lock_info|
|
100
|
+
raise LockError, resource unless lock_info
|
77
101
|
return yield
|
78
102
|
end
|
79
103
|
end
|
80
104
|
|
105
|
+
# Gets remaining ttl of a resource. The ttl is returned if the holder
|
106
|
+
# currently holds the lock and it has not expired, otherwise the method
|
107
|
+
# returns nil.
|
108
|
+
# Params:
|
109
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource
|
110
|
+
def get_remaining_ttl_for_lock(lock_info)
|
111
|
+
ttl_info = try_get_remaining_ttl(lock_info[:resource])
|
112
|
+
return nil if ttl_info.nil? || ttl_info[:value] != lock_info[:value]
|
113
|
+
ttl_info[:ttl]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Gets remaining ttl of a resource. If there is no valid lock, the method
|
117
|
+
# returns nil.
|
118
|
+
# Params:
|
119
|
+
# +resource+:: the name of the resource (string) for which to check the ttl
|
120
|
+
def get_remaining_ttl_for_resource(resource)
|
121
|
+
ttl_info = try_get_remaining_ttl(resource)
|
122
|
+
return nil if ttl_info.nil?
|
123
|
+
ttl_info[:ttl]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Checks if a resource is locked
|
127
|
+
# Params:
|
128
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource
|
129
|
+
def locked?(resource)
|
130
|
+
ttl = get_remaining_ttl_for_resource(resource)
|
131
|
+
!(ttl.nil? || ttl.zero?)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Checks if a lock is still valid
|
135
|
+
# Params:
|
136
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource
|
137
|
+
def valid_lock?(lock_info)
|
138
|
+
ttl = get_remaining_ttl_for_lock(lock_info)
|
139
|
+
!(ttl.nil? || ttl.zero?)
|
140
|
+
end
|
141
|
+
|
81
142
|
private
|
82
143
|
|
83
144
|
class RedisInstance
|
@@ -98,11 +159,26 @@ module Redlock
|
|
98
159
|
end
|
99
160
|
eos
|
100
161
|
|
162
|
+
PTTL_SCRIPT = <<-eos
|
163
|
+
return { redis.call("get", KEYS[1]), redis.call("pttl", KEYS[1]) }
|
164
|
+
eos
|
165
|
+
|
166
|
+
module ConnectionPoolLike
|
167
|
+
def with
|
168
|
+
yield self
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
101
172
|
def initialize(connection)
|
102
|
-
if connection.respond_to?(:
|
173
|
+
if connection.respond_to?(:with)
|
103
174
|
@redis = connection
|
104
175
|
else
|
105
|
-
|
176
|
+
if connection.respond_to?(:client)
|
177
|
+
@redis = connection
|
178
|
+
else
|
179
|
+
@redis = Redis.new(connection)
|
180
|
+
end
|
181
|
+
@redis.extend(ConnectionPoolLike)
|
106
182
|
end
|
107
183
|
|
108
184
|
load_scripts
|
@@ -110,23 +186,34 @@ module Redlock
|
|
110
186
|
|
111
187
|
def lock(resource, val, ttl, allow_new_lock)
|
112
188
|
recover_from_script_flush do
|
113
|
-
@redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
|
189
|
+
@redis.with { |conn| conn.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock] }
|
114
190
|
end
|
191
|
+
rescue Redis::BaseConnectionError
|
192
|
+
false
|
115
193
|
end
|
116
194
|
|
117
195
|
def unlock(resource, val)
|
118
196
|
recover_from_script_flush do
|
119
|
-
@redis.evalsha @unlock_script_sha, keys: [resource], argv: [val]
|
197
|
+
@redis.with { |conn| conn.evalsha @unlock_script_sha, keys: [resource], argv: [val] }
|
120
198
|
end
|
121
199
|
rescue
|
122
200
|
# Nothing to do, unlocking is just a best-effort attempt.
|
123
201
|
end
|
124
202
|
|
203
|
+
def get_remaining_ttl(resource)
|
204
|
+
recover_from_script_flush do
|
205
|
+
@redis.with { |conn| conn.evalsha @pttl_script_sha, keys: [resource] }
|
206
|
+
end
|
207
|
+
rescue Redis::BaseConnectionError
|
208
|
+
nil
|
209
|
+
end
|
210
|
+
|
125
211
|
private
|
126
212
|
|
127
213
|
def load_scripts
|
128
|
-
@unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
|
129
|
-
@lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
|
214
|
+
@unlock_script_sha = @redis.with { |conn| conn.script(:load, UNLOCK_SCRIPT) }
|
215
|
+
@lock_script_sha = @redis.with { |conn| conn.script(:load, LOCK_SCRIPT) }
|
216
|
+
@pttl_script_sha = @redis.with { |conn| conn.script(:load, PTTL_SCRIPT) }
|
130
217
|
end
|
131
218
|
|
132
219
|
def recover_from_script_flush
|
@@ -149,11 +236,11 @@ module Redlock
|
|
149
236
|
end
|
150
237
|
|
151
238
|
def try_lock_instances(resource, ttl, options)
|
152
|
-
tries = options[:extend] ? 1 : @retry_count
|
239
|
+
tries = options[:extend] ? 1 : (@retry_count + 1)
|
153
240
|
|
154
241
|
tries.times do |attempt_number|
|
155
242
|
# Wait a random delay before retrying.
|
156
|
-
sleep((
|
243
|
+
sleep(attempt_retry_delay(attempt_number)) if attempt_number > 0
|
157
244
|
|
158
245
|
lock_info = lock_instances(resource, ttl, options)
|
159
246
|
return lock_info if lock_info
|
@@ -162,9 +249,20 @@ module Redlock
|
|
162
249
|
false
|
163
250
|
end
|
164
251
|
|
252
|
+
def attempt_retry_delay(attempt_number)
|
253
|
+
retry_delay =
|
254
|
+
if @retry_delay.respond_to?(:call)
|
255
|
+
@retry_delay.call(attempt_number)
|
256
|
+
else
|
257
|
+
@retry_delay
|
258
|
+
end
|
259
|
+
|
260
|
+
(retry_delay + rand(@retry_jitter)).to_f / 1000
|
261
|
+
end
|
262
|
+
|
165
263
|
def lock_instances(resource, ttl, options)
|
166
264
|
value = (options[:extend] || { value: SecureRandom.uuid })[:value]
|
167
|
-
allow_new_lock =
|
265
|
+
allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
|
168
266
|
|
169
267
|
locked, time_elapsed = timed do
|
170
268
|
@servers.select { |s| s.lock resource, value, ttl, allow_new_lock }.size
|
@@ -180,6 +278,37 @@ module Redlock
|
|
180
278
|
end
|
181
279
|
end
|
182
280
|
|
281
|
+
def try_get_remaining_ttl(resource)
|
282
|
+
# Responses from the servers are a 2 tuple of format [lock_value, ttl].
|
283
|
+
# The lock_value is nil if it does not exist. Since servers may have
|
284
|
+
# different lock values, the responses are grouped by the lock_value and
|
285
|
+
# transofrmed into a hash: { lock_value1 => [ttl1, ttl2, ttl3],
|
286
|
+
# lock_value2 => [ttl4, tt5] }
|
287
|
+
ttls_by_value, time_elapsed = timed do
|
288
|
+
@servers.map { |s| s.get_remaining_ttl(resource) }
|
289
|
+
.select { |ttl_tuple| ttl_tuple&.first }
|
290
|
+
.group_by(&:first)
|
291
|
+
.transform_values { |ttl_tuples| ttl_tuples.map { |t| t.last } }
|
292
|
+
end
|
293
|
+
|
294
|
+
# Authoritative lock value is that which is returned by the majority of
|
295
|
+
# servers
|
296
|
+
authoritative_value, ttls =
|
297
|
+
ttls_by_value.max_by { |(lock_value, ttls)| ttls.length }
|
298
|
+
|
299
|
+
if ttls && ttls.size >= @quorum
|
300
|
+
# Return the minimum TTL of an N/2+1 selection. It will always be
|
301
|
+
# correct (it will guarantee that at least N/2+1 servers have a TTL that
|
302
|
+
# value or longer)
|
303
|
+
min_ttl = ttls.sort.last(@quorum).first
|
304
|
+
min_ttl = min_ttl - time_elapsed - drift(min_ttl)
|
305
|
+
{ value: authoritative_value, ttl: min_ttl }
|
306
|
+
else
|
307
|
+
# No lock_value is authoritatively held for the resource
|
308
|
+
nil
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
183
312
|
def drift(ttl)
|
184
313
|
# Add 2 milliseconds to the drift to account for Redis expires
|
185
314
|
# precision, which is 1 millisecond, plus 1 millisecond min drift
|
@@ -188,8 +317,8 @@ module Redlock
|
|
188
317
|
end
|
189
318
|
|
190
319
|
def timed
|
191
|
-
start_time = (
|
192
|
-
[yield, (
|
320
|
+
start_time = @time_source.call()
|
321
|
+
[yield, @time_source.call() - start_time]
|
193
322
|
end
|
194
323
|
end
|
195
324
|
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/redlock.gemspec
CHANGED
@@ -1,26 +1,29 @@
|
|
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
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
20
|
+
spec.require_paths = ['lib']
|
20
21
|
|
21
22
|
spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
|
22
23
|
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency '
|
24
|
+
spec.add_development_dependency 'connection_pool', '~> 2.2'
|
25
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
26
|
+
spec.add_development_dependency 'json', '>= 2.3.0', '~> 2.3.1'
|
27
|
+
spec.add_development_dependency 'rake', '>= 11.1.2', '~> 13.0'
|
25
28
|
spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
|
26
29
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,20 +1,31 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'securerandom'
|
3
|
+
require 'redis'
|
4
|
+
require 'connection_pool'
|
3
5
|
|
4
6
|
RSpec.describe Redlock::Client do
|
5
7
|
# It is recommended to have at least 3 servers in production
|
6
8
|
let(:lock_manager_opts) { { retry_count: 3 } }
|
7
9
|
let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
|
10
|
+
let(:redis_client) { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
8
11
|
let(:resource_key) { SecureRandom.hex(3) }
|
9
12
|
let(:ttl) { 1000 }
|
10
13
|
let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
|
11
14
|
let(:redis1_port) { ENV["REDIS1_PORT"] || "6379" }
|
12
15
|
let(:redis2_host) { ENV["REDIS2_HOST"] || "127.0.0.1" }
|
13
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
|
+
}
|
14
26
|
|
15
27
|
describe 'initialize' do
|
16
28
|
it 'accepts both redis URLs and Redis objects' do
|
17
|
-
print redis1_host
|
18
29
|
servers = [ "redis://#{redis1_host}:#{redis1_port}", Redis.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
|
19
30
|
redlock = Redlock::Client.new(servers)
|
20
31
|
|
@@ -24,6 +35,15 @@ RSpec.describe Redlock::Client do
|
|
24
35
|
|
25
36
|
expect(redlock_servers).to match_array([redis1_host, redis2_host])
|
26
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
|
27
47
|
end
|
28
48
|
|
29
49
|
describe 'lock' do
|
@@ -42,6 +62,12 @@ RSpec.describe Redlock::Client do
|
|
42
62
|
expect(@lock_info).to be_lock_info_for(resource_key)
|
43
63
|
end
|
44
64
|
|
65
|
+
it 'interprets lock time as milliseconds' do
|
66
|
+
ttl = 20000
|
67
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
68
|
+
expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
|
69
|
+
end
|
70
|
+
|
45
71
|
it 'can extend its own lock' do
|
46
72
|
my_lock_info = lock_manager.lock(resource_key, ttl)
|
47
73
|
@lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
|
@@ -57,16 +83,26 @@ RSpec.describe Redlock::Client do
|
|
57
83
|
end
|
58
84
|
end
|
59
85
|
|
60
|
-
context 'when
|
86
|
+
context 'when extend_only_if_locked flag is given' do
|
61
87
|
it 'does not extend a non-existent lock' do
|
62
|
-
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'},
|
88
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: true)
|
63
89
|
expect(@lock_info).to eq(false)
|
64
90
|
end
|
65
91
|
end
|
66
92
|
|
67
|
-
|
93
|
+
it '(when extending) resets the TTL, rather than adding extra time to it' do
|
94
|
+
ttl = 20000
|
95
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
96
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
97
|
+
|
98
|
+
lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_locked: true)
|
99
|
+
expect(lock_info).not_to be_nil
|
100
|
+
expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'when extend_only_if_locked flag is not given' do
|
68
104
|
it "sets the given value when trying to extend a non-existent lock" do
|
69
|
-
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'},
|
105
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_locked: false)
|
70
106
|
expect(@lock_info).to be_lock_info_for(resource_key)
|
71
107
|
expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
|
72
108
|
end
|
@@ -77,6 +113,28 @@ RSpec.describe Redlock::Client do
|
|
77
113
|
second_attempt = lock_manager.lock(resource_key, ttl)
|
78
114
|
expect(second_attempt).to eq(false)
|
79
115
|
end
|
116
|
+
|
117
|
+
context 'when extend_life flag is given' do
|
118
|
+
it 'treats it as extend_only_if_locked but warns it is deprecated' do
|
119
|
+
ttl = 20_000
|
120
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
121
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
122
|
+
expect(lock_manager).to receive(:warn).with(/DEPRECATION WARNING: The `extend_life`/)
|
123
|
+
lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
|
124
|
+
expect(lock_info).not_to be_nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when extend_only_if_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_only_if_life`/)
|
134
|
+
lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_only_if_life: true)
|
135
|
+
expect(lock_info).not_to be_nil
|
136
|
+
end
|
137
|
+
end
|
80
138
|
end
|
81
139
|
|
82
140
|
context 'when lock is not available' do
|
@@ -95,14 +153,14 @@ RSpec.describe Redlock::Client do
|
|
95
153
|
expect(lock_info).to eql(false)
|
96
154
|
end
|
97
155
|
|
98
|
-
it '
|
156
|
+
it 'tries up to \'retry_count\' + 1 times' do
|
99
157
|
expect(lock_manager).to receive(:lock_instances).exactly(
|
100
|
-
lock_manager_opts[:retry_count]).times.and_return(false)
|
158
|
+
lock_manager_opts[:retry_count] + 1).times.and_return(false)
|
101
159
|
lock_manager.lock(resource_key, ttl)
|
102
160
|
end
|
103
161
|
|
104
162
|
it 'sleeps in between retries' do
|
105
|
-
expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count]
|
163
|
+
expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count]).times
|
106
164
|
lock_manager.lock(resource_key, ttl)
|
107
165
|
end
|
108
166
|
|
@@ -121,6 +179,46 @@ RSpec.describe Redlock::Client do
|
|
121
179
|
end.at_least(:once)
|
122
180
|
lock_manager.lock(resource_key, ttl)
|
123
181
|
end
|
182
|
+
|
183
|
+
it 'accepts retry_delay as proc' do
|
184
|
+
retry_delay = proc do |attempt_number|
|
185
|
+
expect(attempt_number).to eq(1)
|
186
|
+
2000
|
187
|
+
end
|
188
|
+
|
189
|
+
lock_manager = Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, retry_count: 1, retry_delay: retry_delay)
|
190
|
+
another_lock_info = lock_manager.lock(resource_key, ttl)
|
191
|
+
|
192
|
+
expect(lock_manager).to receive(:sleep) do |sleep|
|
193
|
+
expect(sleep * 1000).to be_within(described_class::DEFAULT_RETRY_JITTER).of(2000)
|
194
|
+
end.exactly(:once)
|
195
|
+
lock_manager.lock(resource_key, ttl)
|
196
|
+
lock_manager.unlock(another_lock_info)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'when a server goes away' do
|
201
|
+
it 'does not raise an error on connection issues' do
|
202
|
+
# We re-route the lock manager to a (hopefully) non-existent Redis URL.
|
203
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
204
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
205
|
+
|
206
|
+
expect {
|
207
|
+
expect(lock_manager.lock(resource_key, ttl)).to be_falsey
|
208
|
+
}.to_not raise_error
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'when a server comes back' do
|
213
|
+
it 'recovers from connection issues' do
|
214
|
+
# Same as above.
|
215
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
216
|
+
old_redis = redis_instance.instance_variable_get(:@redis)
|
217
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
218
|
+
expect(lock_manager.lock(resource_key, ttl)).to be_falsey
|
219
|
+
redis_instance.instance_variable_set(:@redis, old_redis)
|
220
|
+
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
|
221
|
+
end
|
124
222
|
end
|
125
223
|
|
126
224
|
context 'when script cache has been flushed' do
|
@@ -254,7 +352,9 @@ RSpec.describe Redlock::Client do
|
|
254
352
|
after { lock_manager.unlock(@another_lock_info) }
|
255
353
|
|
256
354
|
it 'raises a LockError' do
|
257
|
-
expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(
|
355
|
+
expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(
|
356
|
+
Redlock::LockError, "failed to acquire lock on '#{resource_key}'"
|
357
|
+
)
|
258
358
|
end
|
259
359
|
|
260
360
|
it 'does not execute the block' do
|
@@ -267,4 +367,171 @@ RSpec.describe Redlock::Client do
|
|
267
367
|
end
|
268
368
|
end
|
269
369
|
end
|
370
|
+
|
371
|
+
describe 'get_remaining_ttl_for_resource' do
|
372
|
+
context 'when lock is valid' do
|
373
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
374
|
+
|
375
|
+
it 'gets the remaining ttl of a lock' do
|
376
|
+
ttl = 20_000
|
377
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
378
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
|
379
|
+
expect(remaining_ttl).to be_within(300).of(ttl)
|
380
|
+
end
|
381
|
+
|
382
|
+
context 'when servers respond with varying ttls' do
|
383
|
+
let (:servers) {
|
384
|
+
[
|
385
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
386
|
+
"redis://#{redis2_host}:#{redis2_port}",
|
387
|
+
"redis://#{redis3_host}:#{redis3_port}"
|
388
|
+
]
|
389
|
+
}
|
390
|
+
let (:redlock) { Redlock::Client.new(servers) }
|
391
|
+
after(:each) { redlock.unlock(@lock_info) if @lock_info }
|
392
|
+
|
393
|
+
it 'returns the minimum ttl value' do
|
394
|
+
ttl = 20_000
|
395
|
+
@lock_info = redlock.lock(resource_key, ttl)
|
396
|
+
|
397
|
+
# Mock redis server responses to return different ttls
|
398
|
+
returned_ttls = [20_000, 15_000, 10_000]
|
399
|
+
redlock.instance_variable_get(:@servers).each_with_index do |server, index|
|
400
|
+
allow(server).to(receive(:get_remaining_ttl))
|
401
|
+
.with(resource_key)
|
402
|
+
.and_return([@lock_info[:value], returned_ttls[index]])
|
403
|
+
end
|
404
|
+
|
405
|
+
remaining_ttl = redlock.get_remaining_ttl_for_lock(@lock_info)
|
406
|
+
|
407
|
+
# Assert that the TTL is closest to the closest to the correct value
|
408
|
+
expect(remaining_ttl).to be_within(300).of(returned_ttls[1])
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
context 'when lock is not valid' do
|
414
|
+
it 'returns nil' do
|
415
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
416
|
+
lock_manager.unlock(lock_info)
|
417
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
|
418
|
+
expect(remaining_ttl).to be_nil
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
context 'when server goes away' do
|
423
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
424
|
+
|
425
|
+
it 'does not raise an error on connection issues' do
|
426
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
427
|
+
|
428
|
+
# Replace redis with unreachable instance
|
429
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
430
|
+
old_redis = redis_instance.instance_variable_get(:@redis)
|
431
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
432
|
+
|
433
|
+
expect {
|
434
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_resource(resource_key)
|
435
|
+
expect(remaining_ttl).to be_nil
|
436
|
+
}.to_not raise_error
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
context 'when a server comes back' do
|
441
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
442
|
+
|
443
|
+
it 'recovers from connection issues' do
|
444
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
445
|
+
|
446
|
+
# Replace redis with unreachable instance
|
447
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
448
|
+
old_redis = redis_instance.instance_variable_get(:@redis)
|
449
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
450
|
+
|
451
|
+
expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_nil
|
452
|
+
|
453
|
+
# Restore redis
|
454
|
+
redis_instance.instance_variable_set(:@redis, old_redis)
|
455
|
+
expect(lock_manager.get_remaining_ttl_for_resource(resource_key)).to be_truthy
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
describe 'get_remaining_ttl_for_lock' do
|
461
|
+
context 'when lock is valid' do
|
462
|
+
it 'gets the remaining ttl of a lock' do
|
463
|
+
ttl = 20_000
|
464
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
465
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
|
466
|
+
expect(remaining_ttl).to be_within(300).of(ttl)
|
467
|
+
lock_manager.unlock(lock_info)
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
context 'when lock is not valid' do
|
472
|
+
it 'returns nil' do
|
473
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
474
|
+
lock_manager.unlock(lock_info)
|
475
|
+
remaining_ttl = lock_manager.get_remaining_ttl_for_lock(lock_info)
|
476
|
+
expect(remaining_ttl).to be_nil
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
describe 'locked?' do
|
482
|
+
context 'when lock is available' do
|
483
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
484
|
+
|
485
|
+
it 'returns true' do
|
486
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
487
|
+
expect(lock_manager).to be_locked(resource_key)
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
context 'when lock is not available' do
|
492
|
+
it 'returns false' do
|
493
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
494
|
+
lock_manager.unlock(lock_info)
|
495
|
+
expect(lock_manager).not_to be_locked(resource_key)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
describe 'valid_lock?' do
|
501
|
+
context 'when lock is available' do
|
502
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
503
|
+
|
504
|
+
it 'returns true' do
|
505
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
506
|
+
expect(lock_manager).to be_valid_lock(@lock_info)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
context 'when lock is not available' do
|
511
|
+
it 'returns false' do
|
512
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
513
|
+
lock_manager.unlock(lock_info)
|
514
|
+
expect(lock_manager).not_to be_valid_lock(lock_info)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
describe '#default_time_source' do
|
520
|
+
context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
|
521
|
+
it 'returns a callable using Process.clock_gettime()' do
|
522
|
+
skip 'CLOCK_MONOTONIC not defined' unless defined?(Process::CLOCK_MONOTONIC)
|
523
|
+
expect(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC).and_call_original
|
524
|
+
Redlock::Client.default_time_source.call()
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
context 'when CLOCK_MONOTONIC is not available' do
|
529
|
+
it 'returns a callable using Time.now()' do
|
530
|
+
cm = Process.send(:remove_const, :CLOCK_MONOTONIC)
|
531
|
+
expect(Time).to receive(:now).and_call_original
|
532
|
+
Redlock::Client.default_time_source.call()
|
533
|
+
Process.const_set(:CLOCK_MONOTONIC, cm) if cm
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
270
537
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -41,3 +41,11 @@ RSpec::Matchers.define :be_lockable do |lock_manager, ttl|
|
|
41
41
|
"expected that #{resource_key} would be lockable"
|
42
42
|
end
|
43
43
|
end
|
44
|
+
|
45
|
+
RSpec.configure do |c|
|
46
|
+
# NOTE: this protects against erroneous "focus: true" commits
|
47
|
+
unless ENV['CI'] == 'true'
|
48
|
+
c.filter_run focus: true
|
49
|
+
c.run_all_when_everything_filtered = true
|
50
|
+
end
|
51
|
+
end
|
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:
|
4
|
+
version: 1.2.1
|
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: 2020-12-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -31,39 +31,73 @@ dependencies:
|
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '5.0'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
34
|
+
name: connection_pool
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: '2.2'
|
40
40
|
type: :development
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
46
|
+
version: '2.2'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
48
|
+
name: coveralls
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
|
-
version: '
|
53
|
+
version: '0.8'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.8'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: json
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
54
65
|
- - ">="
|
55
66
|
- !ruby/object:Gem::Version
|
56
|
-
version:
|
67
|
+
version: 2.3.0
|
68
|
+
- - "~>"
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.3.1
|
57
71
|
type: :development
|
58
72
|
prerelease: false
|
59
73
|
version_requirements: !ruby/object:Gem::Requirement
|
60
74
|
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.3.0
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 2.3.1
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rake
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 11.1.2
|
61
88
|
- - "~>"
|
62
89
|
- !ruby/object:Gem::Version
|
63
|
-
version: '
|
90
|
+
version: '13.0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
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
|
@@ -96,7 +130,6 @@ files:
|
|
96
130
|
- ".travis.yml"
|
97
131
|
- CONTRIBUTORS
|
98
132
|
- Gemfile
|
99
|
-
- Gemfile.lock
|
100
133
|
- LICENSE
|
101
134
|
- Makefile
|
102
135
|
- README.md
|
@@ -130,8 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
163
|
- !ruby/object:Gem::Version
|
131
164
|
version: '0'
|
132
165
|
requirements: []
|
133
|
-
|
134
|
-
rubygems_version: 2.5.2.1
|
166
|
+
rubygems_version: 3.1.4
|
135
167
|
signing_key:
|
136
168
|
specification_version: 4
|
137
169
|
summary: Distributed lock using Redis written in Ruby.
|
data/Gemfile.lock
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
redlock (0.2.2)
|
5
|
-
redis (>= 3.0.0, < 5.0)
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
coveralls (0.8.19)
|
11
|
-
json (>= 1.8, < 3)
|
12
|
-
simplecov (~> 0.12.0)
|
13
|
-
term-ansicolor (~> 1.3)
|
14
|
-
thor (~> 0.19.1)
|
15
|
-
tins (~> 1.6)
|
16
|
-
diff-lcs (1.3)
|
17
|
-
docile (1.1.5)
|
18
|
-
json (2.0.3)
|
19
|
-
rake (11.3.0)
|
20
|
-
redis (4.0.1)
|
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.12.0)
|
35
|
-
docile (~> 1.1.0)
|
36
|
-
json (>= 1.8, < 3)
|
37
|
-
simplecov-html (~> 0.10.0)
|
38
|
-
simplecov-html (0.10.0)
|
39
|
-
term-ansicolor (1.4.0)
|
40
|
-
tins (~> 1.0)
|
41
|
-
thor (0.19.4)
|
42
|
-
tins (1.13.0)
|
43
|
-
|
44
|
-
PLATFORMS
|
45
|
-
ruby
|
46
|
-
|
47
|
-
DEPENDENCIES
|
48
|
-
coveralls (~> 0.8.13)
|
49
|
-
rake (~> 11.1, >= 11.1.2)
|
50
|
-
redlock!
|
51
|
-
rspec (~> 3, >= 3.0.0)
|
52
|
-
|
53
|
-
BUNDLED WITH
|
54
|
-
1.16.0
|