redlock 0.2.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +14 -14
- data/README.md +31 -26
- data/lib/redlock/client.rb +20 -4
- data/lib/redlock/version.rb +1 -1
- data/redlock.gemspec +1 -1
- data/spec/client_spec.rb +63 -4
- data/spec/spec_helper.rb +8 -0
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12311731641f60b7873ced7fcb039d073fc21525
|
4
|
+
data.tar.gz: 58bc941bc7bbebc10b3fcc32a32e970c3abcefe8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 719b17fb2701e48d3f574f198bafd078fd0f9b6ec91f686e4ece547e222c1b4bf09f5ee94868f1d1417c2e6c87d5c3b4742a185c13bb005fc83d868b2b5d0bc1
|
7
|
+
data.tar.gz: cc759db7fe3ba5f8cf09f1f1427564d1c338a97dc1959304e7fdc561f2817e5f133a54b381382ca1b96712e3109f41247ffa5da4a40bf6fd05a8bfb2d875832b
|
data/Gemfile.lock
CHANGED
@@ -1,23 +1,23 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
redlock (0.
|
4
|
+
redlock (1.0.0)
|
5
5
|
redis (>= 3.0.0, < 5.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
coveralls (0.8.
|
10
|
+
coveralls (0.8.22)
|
11
11
|
json (>= 1.8, < 3)
|
12
|
-
simplecov (~> 0.
|
12
|
+
simplecov (~> 0.16.1)
|
13
13
|
term-ansicolor (~> 1.3)
|
14
|
-
thor (~> 0.19.
|
14
|
+
thor (~> 0.19.4)
|
15
15
|
tins (~> 1.6)
|
16
16
|
diff-lcs (1.3)
|
17
|
-
docile (1.1
|
18
|
-
json (2.0
|
17
|
+
docile (1.3.1)
|
18
|
+
json (2.1.0)
|
19
19
|
rake (11.3.0)
|
20
|
-
redis (4.0.
|
20
|
+
redis (4.0.3)
|
21
21
|
rspec (3.5.0)
|
22
22
|
rspec-core (~> 3.5.0)
|
23
23
|
rspec-expectations (~> 3.5.0)
|
@@ -31,24 +31,24 @@ GEM
|
|
31
31
|
diff-lcs (>= 1.2.0, < 2.0)
|
32
32
|
rspec-support (~> 3.5.0)
|
33
33
|
rspec-support (3.5.0)
|
34
|
-
simplecov (0.
|
35
|
-
docile (~> 1.1
|
34
|
+
simplecov (0.16.1)
|
35
|
+
docile (~> 1.1)
|
36
36
|
json (>= 1.8, < 3)
|
37
37
|
simplecov-html (~> 0.10.0)
|
38
|
-
simplecov-html (0.10.
|
39
|
-
term-ansicolor (1.
|
38
|
+
simplecov-html (0.10.2)
|
39
|
+
term-ansicolor (1.6.0)
|
40
40
|
tins (~> 1.0)
|
41
41
|
thor (0.19.4)
|
42
|
-
tins (1.
|
42
|
+
tins (1.16.3)
|
43
43
|
|
44
44
|
PLATFORMS
|
45
45
|
ruby
|
46
46
|
|
47
47
|
DEPENDENCIES
|
48
|
-
coveralls (~> 0.8
|
48
|
+
coveralls (~> 0.8)
|
49
49
|
rake (~> 11.1, >= 11.1.2)
|
50
50
|
redlock!
|
51
51
|
rspec (~> 3, >= 3.0.0)
|
52
52
|
|
53
53
|
BUNDLED WITH
|
54
|
-
1.
|
54
|
+
1.17.2
|
data/README.md
CHANGED
@@ -43,30 +43,27 @@ Or install it yourself as:
|
|
43
43
|
|
44
44
|
## Usage example
|
45
45
|
|
46
|
+
### Acquiring a lock
|
47
|
+
|
46
48
|
```ruby
|
47
49
|
# Locking
|
48
50
|
lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
|
49
51
|
first_try_lock_info = lock_manager.lock("resource_key", 2000)
|
50
52
|
second_try_lock_info = lock_manager.lock("resource_key", 2000)
|
51
53
|
|
52
|
-
# it prints lock info {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
|
53
54
|
p first_try_lock_info
|
54
|
-
#
|
55
|
+
# => {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
|
56
|
+
|
55
57
|
p second_try_lock_info
|
58
|
+
# => false
|
56
59
|
|
57
60
|
# Unlocking
|
58
61
|
lock_manager.unlock(first_try_lock_info)
|
62
|
+
|
59
63
|
second_try_lock_info = lock_manager.lock("resource_key", 2000)
|
60
64
|
|
61
|
-
# now it prints lock info
|
62
65
|
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)
|
66
|
+
# => {validity: 1962, resource: "resource_key", value: "generated_uuid5"}
|
70
67
|
```
|
71
68
|
|
72
69
|
There's also a block version that automatically unlocks the lock:
|
@@ -81,7 +78,7 @@ lock_manager.lock("resource_key", 2000) do |locked|
|
|
81
78
|
end
|
82
79
|
```
|
83
80
|
|
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
|
81
|
+
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
82
|
|
86
83
|
```ruby
|
87
84
|
begin
|
@@ -93,34 +90,43 @@ rescue Redlock::LockError
|
|
93
90
|
end
|
94
91
|
```
|
95
92
|
|
93
|
+
### Extending a lock
|
94
|
+
|
96
95
|
To extend the life of the lock:
|
97
96
|
|
98
97
|
```ruby
|
99
98
|
begin
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
99
|
+
lock_info = lock_manager.lock("resource_key", 2000)
|
100
|
+
while lock_info
|
101
|
+
# Critical code
|
102
|
+
|
103
|
+
# Time up and more work to do? Extend the lock.
|
104
|
+
lock_info = lock_manager.lock("resource key", 3000, extend: lock_info)
|
104
105
|
end
|
105
106
|
rescue Redlock::LockError
|
106
107
|
# error handling
|
107
108
|
end
|
108
109
|
```
|
109
110
|
|
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 `extend_life` parameter:
|
111
|
+
The above code will also acquire the lock if the previous lock has expired and the lock is currently free. Keep in mind that this means the lock could have been acquired by someone else in the meantime. To only extend the life of the lock if currently locked by yourself, use the `extend_life` parameter:
|
111
112
|
|
112
113
|
```ruby
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
114
|
+
lock_manager.lock("resource key", 3000, extend: lock_info, extend_life: true)
|
115
|
+
```
|
116
|
+
|
117
|
+
## Redis client configuration
|
118
|
+
|
119
|
+
`Redlock::Client` expects URLs or Redis objects on initialization. Redis objects should be used for configuring the connection in more detail, i.e. setting username and password.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
|
123
|
+
redlock = Redlock::Client.new(servers)
|
122
124
|
```
|
123
125
|
|
126
|
+
Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+.
|
127
|
+
|
128
|
+
## Redlock configuration
|
129
|
+
|
124
130
|
It's possible to customize the retry logic providing the following options:
|
125
131
|
|
126
132
|
```ruby
|
@@ -135,7 +141,6 @@ It's possible to customize the retry logic providing the following options:
|
|
135
141
|
|
136
142
|
For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
|
137
143
|
|
138
|
-
|
139
144
|
## Run tests
|
140
145
|
|
141
146
|
Make sure you have [docker installed](https://docs.docker.com/engine/installation/).
|
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.
|
@@ -112,6 +126,8 @@ module Redlock
|
|
112
126
|
recover_from_script_flush do
|
113
127
|
@redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
|
114
128
|
end
|
129
|
+
rescue Redis::CannotConnectError
|
130
|
+
false
|
115
131
|
end
|
116
132
|
|
117
133
|
def unlock(resource, val)
|
@@ -149,7 +165,7 @@ module Redlock
|
|
149
165
|
end
|
150
166
|
|
151
167
|
def try_lock_instances(resource, ttl, options)
|
152
|
-
tries = options[:extend] ? 1 : @retry_count
|
168
|
+
tries = options[:extend] ? 1 : (@retry_count + 1)
|
153
169
|
|
154
170
|
tries.times do |attempt_number|
|
155
171
|
# Wait a random delay before retrying.
|
@@ -188,8 +204,8 @@ module Redlock
|
|
188
204
|
end
|
189
205
|
|
190
206
|
def timed
|
191
|
-
start_time = (
|
192
|
-
[yield, (
|
207
|
+
start_time = @time_source.call()
|
208
|
+
[yield, @time_source.call() - start_time]
|
193
209
|
end
|
194
210
|
end
|
195
211
|
end
|
data/lib/redlock/version.rb
CHANGED
data/redlock.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
|
22
22
|
|
23
|
-
spec.add_development_dependency "coveralls", "~> 0.8
|
23
|
+
spec.add_development_dependency "coveralls", "~> 0.8"
|
24
24
|
spec.add_development_dependency 'rake', '~> 11.1', '>= 11.1.2'
|
25
25
|
spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
|
26
26
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'securerandom'
|
3
|
+
require 'redis'
|
3
4
|
|
4
5
|
RSpec.describe Redlock::Client do
|
5
6
|
# It is recommended to have at least 3 servers in production
|
6
7
|
let(:lock_manager_opts) { { retry_count: 3 } }
|
7
8
|
let(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
|
9
|
+
let(:redis_client) { Redis.new }
|
8
10
|
let(:resource_key) { SecureRandom.hex(3) }
|
9
11
|
let(:ttl) { 1000 }
|
10
12
|
let(:redis1_host) { ENV["REDIS1_HOST"] || "localhost" }
|
@@ -14,7 +16,6 @@ RSpec.describe Redlock::Client do
|
|
14
16
|
|
15
17
|
describe 'initialize' do
|
16
18
|
it 'accepts both redis URLs and Redis objects' do
|
17
|
-
print redis1_host
|
18
19
|
servers = [ "redis://#{redis1_host}:#{redis1_port}", Redis.new(url: "redis://#{redis2_host}:#{redis2_port}") ]
|
19
20
|
redlock = Redlock::Client.new(servers)
|
20
21
|
|
@@ -42,6 +43,12 @@ RSpec.describe Redlock::Client do
|
|
42
43
|
expect(@lock_info).to be_lock_info_for(resource_key)
|
43
44
|
end
|
44
45
|
|
46
|
+
it 'interprets lock time as milliseconds' do
|
47
|
+
ttl = 20000
|
48
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
49
|
+
expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
|
50
|
+
end
|
51
|
+
|
45
52
|
it 'can extend its own lock' do
|
46
53
|
my_lock_info = lock_manager.lock(resource_key, ttl)
|
47
54
|
@lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
|
@@ -64,6 +71,16 @@ RSpec.describe Redlock::Client do
|
|
64
71
|
end
|
65
72
|
end
|
66
73
|
|
74
|
+
it '(when extending) resets the TTL, rather than adding extra time to it' do
|
75
|
+
ttl = 20000
|
76
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
77
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
78
|
+
|
79
|
+
lock_info = lock_manager.lock(resource_key, ttl, extend: lock_info, extend_life: true)
|
80
|
+
expect(lock_info).not_to be_nil
|
81
|
+
expect(redis_client.pttl(resource_key)).to be_within(200).of(ttl)
|
82
|
+
end
|
83
|
+
|
67
84
|
context 'when extend_only_if_life flag is not given' do
|
68
85
|
it "sets the given value when trying to extend a non-existent lock" do
|
69
86
|
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'}, extend_only_if_life: false)
|
@@ -95,14 +112,14 @@ RSpec.describe Redlock::Client do
|
|
95
112
|
expect(lock_info).to eql(false)
|
96
113
|
end
|
97
114
|
|
98
|
-
it '
|
115
|
+
it 'tries up to \'retry_count\' + 1 times' do
|
99
116
|
expect(lock_manager).to receive(:lock_instances).exactly(
|
100
|
-
lock_manager_opts[:retry_count]).times.and_return(false)
|
117
|
+
lock_manager_opts[:retry_count] + 1).times.and_return(false)
|
101
118
|
lock_manager.lock(resource_key, ttl)
|
102
119
|
end
|
103
120
|
|
104
121
|
it 'sleeps in between retries' do
|
105
|
-
expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count]
|
122
|
+
expect(lock_manager).to receive(:sleep).exactly(lock_manager_opts[:retry_count]).times
|
106
123
|
lock_manager.lock(resource_key, ttl)
|
107
124
|
end
|
108
125
|
|
@@ -123,6 +140,29 @@ RSpec.describe Redlock::Client do
|
|
123
140
|
end
|
124
141
|
end
|
125
142
|
|
143
|
+
context 'when a server goes away' do
|
144
|
+
it 'does not raise an error on connection issues' do
|
145
|
+
# We re-route the lock manager to a (hopefully) non-existent Redis URL.
|
146
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
147
|
+
redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
|
148
|
+
|
149
|
+
expect {
|
150
|
+
expect(lock_manager.lock(resource_key, ttl)).to be_falsey
|
151
|
+
}.to_not raise_error
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'when a server comes back' do
|
156
|
+
it 'recovers from connection issues' do
|
157
|
+
# Same as above.
|
158
|
+
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
159
|
+
redis_instance.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
|
160
|
+
expect(lock_manager.lock(resource_key, ttl)).to be_falsey
|
161
|
+
redis_instance.instance_variable_set(:@redis, Redis.new(url: "redis://#{redis1_host}:#{redis1_port}"))
|
162
|
+
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
126
166
|
context 'when script cache has been flushed' do
|
127
167
|
before(:each) do
|
128
168
|
@manipulated_instance = lock_manager.instance_variable_get(:@servers).first
|
@@ -267,4 +307,23 @@ RSpec.describe Redlock::Client do
|
|
267
307
|
end
|
268
308
|
end
|
269
309
|
end
|
310
|
+
|
311
|
+
describe '#default_time_source' do
|
312
|
+
context 'when CLOCK_MONOTONIC is available (MRI, JRuby)' do
|
313
|
+
it 'returns a callable using Process.clock_gettime()' do
|
314
|
+
skip 'CLOCK_MONOTONIC not defined' unless defined?(Process::CLOCK_MONOTONIC)
|
315
|
+
expect(Process).to receive(:clock_gettime).with(Process::CLOCK_MONOTONIC).and_call_original
|
316
|
+
Redlock::Client.default_time_source.call()
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
context 'when CLOCK_MONOTONIC is not available' do
|
321
|
+
it 'returns a callable using Time.now()' do
|
322
|
+
cm = Process.send(:remove_const, :CLOCK_MONOTONIC)
|
323
|
+
expect(Time).to receive(:now).and_call_original
|
324
|
+
Redlock::Client.default_time_source.call()
|
325
|
+
Process.const_set(:CLOCK_MONOTONIC, cm) if cm
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
270
329
|
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: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro Moreira
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -36,14 +36,14 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: 0.8
|
39
|
+
version: '0.8'
|
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: 0.8
|
46
|
+
version: '0.8'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rake
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -131,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
131
|
version: '0'
|
132
132
|
requirements: []
|
133
133
|
rubyforge_project:
|
134
|
-
rubygems_version: 2.5.2.
|
134
|
+
rubygems_version: 2.5.2.3
|
135
135
|
signing_key:
|
136
136
|
specification_version: 4
|
137
137
|
summary: Distributed lock using Redis written in Ruby.
|