redlock 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +13 -5
- data/README.md +13 -1
- data/lib/redlock.rb +2 -0
- data/lib/redlock/client.rb +33 -9
- data/lib/redlock/testing.rb +9 -8
- data/lib/redlock/version.rb +1 -1
- data/spec/client_spec.rb +68 -0
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MWU3MmRhNjM0OWFkYTMyOTMzNzBiNDFjYjFmZDkwOGRhNGFkMzgzMQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
M2EyYWE0YTk5NTg3MThiZWIzNjA2OGRhMjE3MzY1ZjBmMmU2OTI2Zg==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
MDI3YjUyYjQxOTJmNzAyZTE5MGY3MjlhNWYyMjVmZDE4YWQ1YTRkMDA0ODcw
|
10
|
+
M2RlYmQ0NTA3NGUwZjdlYWM0MjM1MjFiNjY4N2YwMzkwM2I5M2MxZjM5OWVi
|
11
|
+
YzdlYzVmOTBlOTRkNzRhNjNhYjNhYzU0OGU1ZTJhNzg0NjUxMjQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MDFmYTU0MjRhZWFhOGY2NTM5YmM4NmQ2ZTA2MDI5NzQ1NjVlZjc2ODhhM2Nm
|
14
|
+
N2JkNmJlZTI5ODU3NzM3MmIxZjUxY2M5ZmZhOTNiOThiNzdmOGI1Zjc2ZTI3
|
15
|
+
ZDkxZGExNTBkY2Y3MGMyMmU0M2U4MmM4OWE5MWZhNTM5Mjg5ODQ=
|
data/README.md
CHANGED
@@ -79,11 +79,23 @@ lock_manager.lock("resource_key", 2000) do |locked|
|
|
79
79
|
end
|
80
80
|
```
|
81
81
|
|
82
|
+
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:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
begin
|
86
|
+
block_result = lock_manager.lock!("resource_key", 2000) do
|
87
|
+
# critical code
|
88
|
+
end
|
89
|
+
rescue Redlock::LockException
|
90
|
+
# error handling
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
82
94
|
## Run tests
|
83
95
|
|
84
96
|
Make sure you have at least 1 redis instances up.
|
85
97
|
|
86
|
-
|
98
|
+
$ rspec
|
87
99
|
|
88
100
|
## Disclaimer
|
89
101
|
|
data/lib/redlock.rb
CHANGED
data/lib/redlock/client.rb
CHANGED
@@ -34,9 +34,11 @@ module Redlock
|
|
34
34
|
# Params:
|
35
35
|
# +resource+:: the resource (or key) string to be locked.
|
36
36
|
# +ttl+:: The time-to-live in ms for the lock.
|
37
|
-
# +
|
38
|
-
|
39
|
-
|
37
|
+
# +extend+: A lock ("lock_info") to extend.
|
38
|
+
# +block+:: an optional block to be executed; after its execution, the lock (if successfully
|
39
|
+
# acquired) is automatically unlocked.
|
40
|
+
def lock(resource, ttl, extend: nil, &block)
|
41
|
+
lock_info = try_lock_instances(resource, ttl, extend)
|
40
42
|
|
41
43
|
if block_given?
|
42
44
|
begin
|
@@ -57,6 +59,18 @@ module Redlock
|
|
57
59
|
@servers.each { |s| s.unlock(lock_info[:resource], lock_info[:value]) }
|
58
60
|
end
|
59
61
|
|
62
|
+
# Locks a resource, executing the received block only after successfully acquiring the lock,
|
63
|
+
# and returning its return value as a result.
|
64
|
+
# See Redlock::Client#lock for parameters.
|
65
|
+
def lock!(*args, **keyword_args)
|
66
|
+
fail 'No block passed' unless block_given?
|
67
|
+
|
68
|
+
lock(*args, **keyword_args) do |lock_info|
|
69
|
+
raise LockError, 'failed to acquire lock' unless lock_info
|
70
|
+
return yield
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
60
74
|
private
|
61
75
|
|
62
76
|
class RedisInstance
|
@@ -68,6 +82,15 @@ module Redlock
|
|
68
82
|
end
|
69
83
|
eos
|
70
84
|
|
85
|
+
# thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
|
86
|
+
# also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
|
87
|
+
# and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
|
88
|
+
LOCK_SCRIPT = <<-eos
|
89
|
+
if redis.call("exists", KEYS[1]) == 0 or redis.call("get", KEYS[1]) == ARGV[1] then
|
90
|
+
return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
|
91
|
+
end
|
92
|
+
eos
|
93
|
+
|
71
94
|
def initialize(connection)
|
72
95
|
if connection.respond_to?(:client)
|
73
96
|
@redis = connection
|
@@ -79,7 +102,7 @@ module Redlock
|
|
79
102
|
end
|
80
103
|
|
81
104
|
def lock(resource, val, ttl)
|
82
|
-
@redis.
|
105
|
+
@redis.evalsha(@lock_script_sha, keys: [resource], argv: [val, ttl])
|
83
106
|
end
|
84
107
|
|
85
108
|
def unlock(resource, val)
|
@@ -92,12 +115,13 @@ module Redlock
|
|
92
115
|
|
93
116
|
def load_scripts
|
94
117
|
@unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
|
118
|
+
@lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
|
95
119
|
end
|
96
120
|
end
|
97
121
|
|
98
|
-
def try_lock_instances(resource, ttl)
|
122
|
+
def try_lock_instances(resource, ttl, extend)
|
99
123
|
@retry_count.times do
|
100
|
-
lock_info = lock_instances(resource, ttl)
|
124
|
+
lock_info = lock_instances(resource, ttl, extend)
|
101
125
|
return lock_info if lock_info
|
102
126
|
|
103
127
|
# Wait a random delay before retrying
|
@@ -107,8 +131,8 @@ module Redlock
|
|
107
131
|
false
|
108
132
|
end
|
109
133
|
|
110
|
-
def lock_instances(resource, ttl)
|
111
|
-
value = SecureRandom.uuid
|
134
|
+
def lock_instances(resource, ttl, extend)
|
135
|
+
value = extend ? extend.fetch(:value) : SecureRandom.uuid
|
112
136
|
|
113
137
|
locked, time_elapsed = timed do
|
114
138
|
@servers.select { |s| s.lock(resource, value, ttl) }.size
|
@@ -128,7 +152,7 @@ module Redlock
|
|
128
152
|
# Add 2 milliseconds to the drift to account for Redis expires
|
129
153
|
# precision, which is 1 millisecond, plus 1 millisecond min drift
|
130
154
|
# for small TTLs.
|
131
|
-
|
155
|
+
(ttl * CLOCK_DRIFT_FACTOR).to_i + 2
|
132
156
|
end
|
133
157
|
|
134
158
|
def timed
|
data/lib/redlock/testing.rb
CHANGED
@@ -4,17 +4,17 @@ module Redlock
|
|
4
4
|
|
5
5
|
alias_method :try_lock_instances_without_testing, :try_lock_instances
|
6
6
|
|
7
|
-
def try_lock_instances(resource, ttl)
|
7
|
+
def try_lock_instances(resource, ttl, extend)
|
8
8
|
if @testing_mode == :bypass
|
9
9
|
{
|
10
10
|
validity: ttl,
|
11
11
|
resource: resource,
|
12
|
-
value: SecureRandom.uuid
|
12
|
+
value: extend ? extend.fetch(:value) : SecureRandom.uuid
|
13
13
|
}
|
14
14
|
elsif @testing_mode == :fail
|
15
15
|
false
|
16
16
|
else
|
17
|
-
try_lock_instances_without_testing resource, ttl
|
17
|
+
try_lock_instances_without_testing resource, ttl, extend
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -25,12 +25,13 @@ module Redlock
|
|
25
25
|
end
|
26
26
|
|
27
27
|
class RedisInstance
|
28
|
+
alias_method :load_scripts_without_testing, :load_scripts
|
29
|
+
|
28
30
|
def load_scripts
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
end
|
31
|
+
load_scripts_without_testing
|
32
|
+
rescue Redis::CommandError
|
33
|
+
# FakeRedis doesn't have #script, but doesn't need it either.
|
34
|
+
raise unless defined?(::FakeRedis)
|
34
35
|
end
|
35
36
|
end
|
36
37
|
end
|
data/lib/redlock/version.rb
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -35,6 +35,25 @@ RSpec.describe Redlock::Client do
|
|
35
35
|
|
36
36
|
expect(@lock_info).to be_lock_info_for(resource_key)
|
37
37
|
end
|
38
|
+
|
39
|
+
it 'can extend its own lock' do
|
40
|
+
my_lock_info = lock_manager.lock(resource_key, ttl)
|
41
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
|
42
|
+
expect(@lock_info).to be_lock_info_for(resource_key)
|
43
|
+
expect(@lock_info[:value]).to eq(my_lock_info[:value])
|
44
|
+
end
|
45
|
+
|
46
|
+
it "sets the given value when trying to extend a non-existent lock" do
|
47
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'})
|
48
|
+
expect(@lock_info).to be_lock_info_for(resource_key)
|
49
|
+
expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
|
50
|
+
end
|
51
|
+
|
52
|
+
it "doesn't extend somebody else's lock" do
|
53
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
54
|
+
second_attempt = lock_manager.lock(resource_key, ttl)
|
55
|
+
expect(second_attempt).to eq(false)
|
56
|
+
end
|
38
57
|
end
|
39
58
|
|
40
59
|
context 'when lock is not available' do
|
@@ -46,6 +65,12 @@ RSpec.describe Redlock::Client do
|
|
46
65
|
|
47
66
|
expect(lock_info).to eql(false)
|
48
67
|
end
|
68
|
+
|
69
|
+
it "can't extend somebody else's lock" do
|
70
|
+
yet_another_lock_info = @another_lock_info.merge value: 'gibberish'
|
71
|
+
lock_info = lock_manager.lock(resource_key, ttl, extend: yet_another_lock_info)
|
72
|
+
expect(lock_info).to eql(false)
|
73
|
+
end
|
49
74
|
end
|
50
75
|
|
51
76
|
describe 'block syntax' do
|
@@ -107,4 +132,47 @@ RSpec.describe Redlock::Client do
|
|
107
132
|
expect(resource_key).to be_lockable(lock_manager, ttl)
|
108
133
|
end
|
109
134
|
end
|
135
|
+
|
136
|
+
describe 'lock!' do
|
137
|
+
context 'when lock is available' do
|
138
|
+
it 'locks' do
|
139
|
+
lock_manager.lock!(resource_key, ttl) do
|
140
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
it "returns the received block's return value" do
|
145
|
+
rv = lock_manager.lock!(resource_key, ttl) { :success }
|
146
|
+
expect(rv).to eql(:success)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'automatically unlocks' do
|
150
|
+
lock_manager.lock!(resource_key, ttl) {}
|
151
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'automatically unlocks when block raises exception' do
|
155
|
+
lock_manager.lock!(resource_key, ttl) { fail } rescue nil
|
156
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
context 'when lock is not available' do
|
161
|
+
before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
|
162
|
+
after { lock_manager.unlock(@another_lock_info) }
|
163
|
+
|
164
|
+
it 'raises a LockError' do
|
165
|
+
expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(Redlock::LockError)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'does not execute the block' do
|
169
|
+
expect do
|
170
|
+
begin
|
171
|
+
lock_manager.lock!(resource_key, ttl) { fail }
|
172
|
+
rescue Redlock::LockError
|
173
|
+
end
|
174
|
+
end.to_not raise_error
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
110
178
|
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.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro Moreira
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-09
|
11
|
+
date: 2015-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -17,7 +17,7 @@ dependencies:
|
|
17
17
|
- - ~>
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '3'
|
20
|
-
- - '>='
|
20
|
+
- - ! '>='
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: 3.0.5
|
23
23
|
type: :runtime
|
@@ -27,7 +27,7 @@ dependencies:
|
|
27
27
|
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '3'
|
30
|
-
- - '>='
|
30
|
+
- - ! '>='
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 3.0.5
|
33
33
|
- !ruby/object:Gem::Dependency
|
@@ -48,14 +48,14 @@ dependencies:
|
|
48
48
|
name: coveralls
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
|
-
- - '>='
|
51
|
+
- - ! '>='
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '0'
|
54
54
|
type: :development
|
55
55
|
prerelease: false
|
56
56
|
version_requirements: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
|
-
- - '>='
|
58
|
+
- - ! '>='
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0'
|
61
61
|
- !ruby/object:Gem::Dependency
|
@@ -120,12 +120,12 @@ require_paths:
|
|
120
120
|
- lib
|
121
121
|
required_ruby_version: !ruby/object:Gem::Requirement
|
122
122
|
requirements:
|
123
|
-
- - '>='
|
123
|
+
- - ! '>='
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '0'
|
126
126
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
127
|
requirements:
|
128
|
-
- - '>='
|
128
|
+
- - ! '>='
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: '0'
|
131
131
|
requirements: []
|