redlock 1.1.0 → 1.2.0
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 +4 -4
- data/Gemfile.lock +5 -3
- data/README.md +8 -2
- data/lib/redlock.rb +5 -1
- data/lib/redlock/client.rb +32 -10
- data/lib/redlock/testing.rb +15 -5
- data/lib/redlock/version.rb +1 -1
- data/redlock.gemspec +2 -1
- data/spec/client_spec.rb +41 -4
- metadata +27 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f27e3a9be9009072c86f2f38a18bdfdcd2f177890aac365d981711dc040acd2e
|
4
|
+
data.tar.gz: efedd1080833cdee147afe6427878ad0df7c824e981e4e8a48c563ac68350d05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aed115da4ab51ae932533d73e2f2f79d7722e6d77471028bb685d89149d6085fd395af368e9a143231b25b680a57c1ad4d236365396974c12416dac1131cabd8
|
7
|
+
data.tar.gz: 75b69cd60c2f562b087179dc939c2fb2bbf3514018dd8c302cf4bc2c4cfa4352a1310dd7cfc35042badcd8c86fc5d237d3a6bcafc57385ca2afcc5a2a280c7bf
|
data/Gemfile.lock
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
redlock (1.
|
4
|
+
redlock (1.2.0)
|
5
5
|
redis (>= 3.0.0, < 5.0)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
+
connection_pool (2.2.2)
|
10
11
|
coveralls (0.8.22)
|
11
12
|
json (>= 1.8, < 3)
|
12
13
|
simplecov (~> 0.16.1)
|
@@ -16,7 +17,7 @@ GEM
|
|
16
17
|
diff-lcs (1.3)
|
17
18
|
docile (1.3.1)
|
18
19
|
json (2.1.0)
|
19
|
-
rake (
|
20
|
+
rake (13.0.1)
|
20
21
|
redis (4.1.1)
|
21
22
|
rspec (3.5.0)
|
22
23
|
rspec-core (~> 3.5.0)
|
@@ -45,8 +46,9 @@ PLATFORMS
|
|
45
46
|
ruby
|
46
47
|
|
47
48
|
DEPENDENCIES
|
49
|
+
connection_pool (~> 2.2)
|
48
50
|
coveralls (~> 0.8)
|
49
|
-
rake (~>
|
51
|
+
rake (~> 13.0, >= 11.1.2)
|
50
52
|
redlock!
|
51
53
|
rspec (~> 3, >= 3.0.0)
|
52
54
|
|
data/README.md
CHANGED
@@ -1,11 +1,9 @@
|
|
1
|
-
[](https://waffle.io/leandromoreira/redlock-rb)
|
2
1
|
[](https://travis-ci.org/leandromoreira/redlock-rb)
|
3
2
|
[](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
|
4
3
|
[](https://codeclimate.com/github/leandromoreira/redlock-rb)
|
5
4
|
[](http://badge.fury.io/rb/redlock)
|
6
5
|
[](https://hakiri.io/github/leandromoreira/redlock-rb/master)
|
7
6
|
[](http://inch-ci.org/github/leandromoreira/redlock-rb)
|
8
|
-
[](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.
|
@@ -139,6 +137,14 @@ It's possible to customize the retry logic providing the following options:
|
|
139
137
|
})
|
140
138
|
```
|
141
139
|
|
140
|
+
It is possible to associate `:retry_delay` option with `Proc` object. It will be called every time, with attempt number
|
141
|
+
as argument, to get delay time value before next retry.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
retry_delay = proc { |attempt_number| 200 * attempt_number ** 2 } # delay of 200ms for 1st retry, 800ms for 2nd retry, etc.
|
145
|
+
lock_manager = Redlock::Client.new(servers, retry_delay: retry_delay)
|
146
|
+
```
|
147
|
+
|
142
148
|
For more information you can check [documentation](http://www.rubydoc.info/gems/redlock/Redlock%2FClient:initialize).
|
143
149
|
|
144
150
|
## Run tests
|
data/lib/redlock.rb
CHANGED
data/lib/redlock/client.rb
CHANGED
@@ -93,11 +93,11 @@ module Redlock
|
|
93
93
|
# Locks a resource, executing the received block only after successfully acquiring the lock,
|
94
94
|
# and returning its return value as a result.
|
95
95
|
# See Redlock::Client#lock for parameters.
|
96
|
-
def lock!(*args)
|
96
|
+
def lock!(resource, *args)
|
97
97
|
fail 'No block passed' unless block_given?
|
98
98
|
|
99
|
-
lock(*args) do |lock_info|
|
100
|
-
raise LockError,
|
99
|
+
lock(resource, *args) do |lock_info|
|
100
|
+
raise LockError, resource unless lock_info
|
101
101
|
return yield
|
102
102
|
end
|
103
103
|
end
|
@@ -122,11 +122,22 @@ module Redlock
|
|
122
122
|
end
|
123
123
|
eos
|
124
124
|
|
125
|
+
module ConnectionPoolLike
|
126
|
+
def with
|
127
|
+
yield self
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
125
131
|
def initialize(connection)
|
126
|
-
if connection.respond_to?(:
|
132
|
+
if connection.respond_to?(:with)
|
127
133
|
@redis = connection
|
128
134
|
else
|
129
|
-
|
135
|
+
if connection.respond_to?(:client)
|
136
|
+
@redis = connection
|
137
|
+
else
|
138
|
+
@redis = Redis.new(connection)
|
139
|
+
end
|
140
|
+
@redis.extend(ConnectionPoolLike)
|
130
141
|
end
|
131
142
|
|
132
143
|
load_scripts
|
@@ -134,7 +145,7 @@ module Redlock
|
|
134
145
|
|
135
146
|
def lock(resource, val, ttl, allow_new_lock)
|
136
147
|
recover_from_script_flush do
|
137
|
-
@redis.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock]
|
148
|
+
@redis.with { |conn| conn.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock] }
|
138
149
|
end
|
139
150
|
rescue Redis::BaseConnectionError
|
140
151
|
false
|
@@ -142,7 +153,7 @@ module Redlock
|
|
142
153
|
|
143
154
|
def unlock(resource, val)
|
144
155
|
recover_from_script_flush do
|
145
|
-
@redis.evalsha @unlock_script_sha, keys: [resource], argv: [val]
|
156
|
+
@redis.with { |conn| conn.evalsha @unlock_script_sha, keys: [resource], argv: [val] }
|
146
157
|
end
|
147
158
|
rescue
|
148
159
|
# Nothing to do, unlocking is just a best-effort attempt.
|
@@ -151,8 +162,8 @@ module Redlock
|
|
151
162
|
private
|
152
163
|
|
153
164
|
def load_scripts
|
154
|
-
@unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
|
155
|
-
@lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
|
165
|
+
@unlock_script_sha = @redis.with { |conn| conn.script(:load, UNLOCK_SCRIPT) }
|
166
|
+
@lock_script_sha = @redis.with { |conn| conn.script(:load, LOCK_SCRIPT) }
|
156
167
|
end
|
157
168
|
|
158
169
|
def recover_from_script_flush
|
@@ -179,7 +190,7 @@ module Redlock
|
|
179
190
|
|
180
191
|
tries.times do |attempt_number|
|
181
192
|
# Wait a random delay before retrying.
|
182
|
-
sleep((
|
193
|
+
sleep(attempt_retry_delay(attempt_number)) if attempt_number > 0
|
183
194
|
|
184
195
|
lock_info = lock_instances(resource, ttl, options)
|
185
196
|
return lock_info if lock_info
|
@@ -188,6 +199,17 @@ module Redlock
|
|
188
199
|
false
|
189
200
|
end
|
190
201
|
|
202
|
+
def attempt_retry_delay(attempt_number)
|
203
|
+
retry_delay =
|
204
|
+
if @retry_delay.respond_to?(:call)
|
205
|
+
@retry_delay.call(attempt_number)
|
206
|
+
else
|
207
|
+
@retry_delay
|
208
|
+
end
|
209
|
+
|
210
|
+
(retry_delay + rand(@retry_jitter)).to_f / 1000
|
211
|
+
end
|
212
|
+
|
191
213
|
def lock_instances(resource, ttl, options)
|
192
214
|
value = (options[:extend] || { value: SecureRandom.uuid })[:value]
|
193
215
|
allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
|
data/lib/redlock/testing.rb
CHANGED
@@ -1,17 +1,27 @@
|
|
1
1
|
module Redlock
|
2
2
|
class Client
|
3
|
-
|
3
|
+
class << self
|
4
|
+
attr_accessor :testing_mode
|
5
|
+
end
|
6
|
+
|
7
|
+
def testing_mode=(mode)
|
8
|
+
warn 'DEPRECATION WARNING: Instance-level `testing_mode` has been removed, and this ' +
|
9
|
+
'setter will be removed in the future. Please set the testing mode on the `Redlock::Client` ' +
|
10
|
+
'instead, e.g. `Redlock::Client.testing_mode = :bypass`.'
|
11
|
+
|
12
|
+
self.class.testing_mode = mode
|
13
|
+
end
|
4
14
|
|
5
15
|
alias_method :try_lock_instances_without_testing, :try_lock_instances
|
6
16
|
|
7
17
|
def try_lock_instances(resource, ttl, options)
|
8
|
-
if
|
18
|
+
if self.class.testing_mode == :bypass
|
9
19
|
{
|
10
20
|
validity: ttl,
|
11
21
|
resource: resource,
|
12
22
|
value: options[:extend] ? options[:extend].fetch(:value) : SecureRandom.uuid
|
13
23
|
}
|
14
|
-
elsif
|
24
|
+
elsif self.class.testing_mode == :fail
|
15
25
|
false
|
16
26
|
else
|
17
27
|
try_lock_instances_without_testing resource, ttl, options
|
@@ -21,14 +31,14 @@ module Redlock
|
|
21
31
|
alias_method :unlock_without_testing, :unlock
|
22
32
|
|
23
33
|
def unlock(lock_info)
|
24
|
-
unlock_without_testing lock_info unless
|
34
|
+
unlock_without_testing lock_info unless self.class.testing_mode == :bypass
|
25
35
|
end
|
26
36
|
|
27
37
|
class RedisInstance
|
28
38
|
alias_method :load_scripts_without_testing, :load_scripts
|
29
39
|
|
30
40
|
def load_scripts
|
31
|
-
load_scripts_without_testing
|
41
|
+
load_scripts_without_testing unless Redlock::Client.testing_mode == :bypass
|
32
42
|
rescue Redis::CommandError
|
33
43
|
# FakeRedis doesn't have #script, but doesn't need it either.
|
34
44
|
raise unless defined?(::FakeRedis)
|
data/lib/redlock/version.rb
CHANGED
data/redlock.gemspec
CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
|
22
22
|
|
23
23
|
spec.add_development_dependency "coveralls", "~> 0.8"
|
24
|
-
spec.add_development_dependency 'rake', '
|
24
|
+
spec.add_development_dependency 'rake', '>= 11.1.2', '~> 13.0'
|
25
25
|
spec.add_development_dependency 'rspec', '~> 3', '>= 3.0.0'
|
26
|
+
spec.add_development_dependency 'connection_pool', '~> 2.2'
|
26
27
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
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
|
@@ -25,6 +26,15 @@ RSpec.describe Redlock::Client do
|
|
25
26
|
|
26
27
|
expect(redlock_servers).to match_array([redis1_host, redis2_host])
|
27
28
|
end
|
29
|
+
|
30
|
+
it 'accepts ConnectionPool objects' do
|
31
|
+
pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
32
|
+
redlock = Redlock::Client.new([pool])
|
33
|
+
|
34
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
35
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
36
|
+
lock_manager.unlock(lock_info)
|
37
|
+
end
|
28
38
|
end
|
29
39
|
|
30
40
|
describe 'lock' do
|
@@ -160,13 +170,29 @@ RSpec.describe Redlock::Client do
|
|
160
170
|
end.at_least(:once)
|
161
171
|
lock_manager.lock(resource_key, ttl)
|
162
172
|
end
|
173
|
+
|
174
|
+
it 'accepts retry_delay as proc' do
|
175
|
+
retry_delay = proc do |attempt_number|
|
176
|
+
expect(attempt_number).to eq(1)
|
177
|
+
2000
|
178
|
+
end
|
179
|
+
|
180
|
+
lock_manager = Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, retry_count: 1, retry_delay: retry_delay)
|
181
|
+
another_lock_info = lock_manager.lock(resource_key, ttl)
|
182
|
+
|
183
|
+
expect(lock_manager).to receive(:sleep) do |sleep|
|
184
|
+
expect(sleep * 1000).to be_within(described_class::DEFAULT_RETRY_JITTER).of(2000)
|
185
|
+
end.exactly(:once)
|
186
|
+
lock_manager.lock(resource_key, ttl)
|
187
|
+
lock_manager.unlock(another_lock_info)
|
188
|
+
end
|
163
189
|
end
|
164
190
|
|
165
191
|
context 'when a server goes away' do
|
166
192
|
it 'does not raise an error on connection issues' do
|
167
193
|
# We re-route the lock manager to a (hopefully) non-existent Redis URL.
|
168
194
|
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
169
|
-
redis_instance.instance_variable_set(:@redis,
|
195
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
170
196
|
|
171
197
|
expect {
|
172
198
|
expect(lock_manager.lock(resource_key, ttl)).to be_falsey
|
@@ -178,13 +204,22 @@ RSpec.describe Redlock::Client do
|
|
178
204
|
it 'recovers from connection issues' do
|
179
205
|
# Same as above.
|
180
206
|
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
181
|
-
redis_instance.
|
207
|
+
old_redis = redis_instance.instance_variable_get(:@redis)
|
208
|
+
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
182
209
|
expect(lock_manager.lock(resource_key, ttl)).to be_falsey
|
183
|
-
redis_instance.instance_variable_set(:@redis,
|
210
|
+
redis_instance.instance_variable_set(:@redis, old_redis)
|
184
211
|
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
|
185
212
|
end
|
186
213
|
end
|
187
214
|
|
215
|
+
def unreachable_redis
|
216
|
+
redis = Redis.new(url: 'redis://localhost:46864')
|
217
|
+
def redis.with
|
218
|
+
yield self
|
219
|
+
end
|
220
|
+
redis
|
221
|
+
end
|
222
|
+
|
188
223
|
context 'when script cache has been flushed' do
|
189
224
|
before(:each) do
|
190
225
|
@manipulated_instance = lock_manager.instance_variable_get(:@servers).first
|
@@ -316,7 +351,9 @@ RSpec.describe Redlock::Client do
|
|
316
351
|
after { lock_manager.unlock(@another_lock_info) }
|
317
352
|
|
318
353
|
it 'raises a LockError' do
|
319
|
-
expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(
|
354
|
+
expect { lock_manager.lock!(resource_key, ttl) {} }.to raise_error(
|
355
|
+
Redlock::LockError, "failed to acquire lock on '#{resource_key}'"
|
356
|
+
)
|
320
357
|
end
|
321
358
|
|
322
359
|
it 'does not execute the block' 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.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: 2020-02-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -48,42 +48,56 @@ dependencies:
|
|
48
48
|
name: rake
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
|
-
- - "~>"
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '11.1'
|
54
51
|
- - ">="
|
55
52
|
- !ruby/object:Gem::Version
|
56
53
|
version: 11.1.2
|
54
|
+
- - "~>"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '13.0'
|
57
57
|
type: :development
|
58
58
|
prerelease: false
|
59
59
|
version_requirements: !ruby/object:Gem::Requirement
|
60
60
|
requirements:
|
61
|
-
- - "~>"
|
62
|
-
- !ruby/object:Gem::Version
|
63
|
-
version: '11.1'
|
64
61
|
- - ">="
|
65
62
|
- !ruby/object:Gem::Version
|
66
63
|
version: 11.1.2
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '13.0'
|
67
67
|
- !ruby/object:Gem::Dependency
|
68
68
|
name: rspec
|
69
69
|
requirement: !ruby/object:Gem::Requirement
|
70
70
|
requirements:
|
71
|
-
- - ">="
|
72
|
-
- !ruby/object:Gem::Version
|
73
|
-
version: 3.0.0
|
74
71
|
- - "~>"
|
75
72
|
- !ruby/object:Gem::Version
|
76
73
|
version: '3'
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.0.0
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '3'
|
81
84
|
- - ">="
|
82
85
|
- !ruby/object:Gem::Version
|
83
86
|
version: 3.0.0
|
87
|
+
- !ruby/object:Gem::Dependency
|
88
|
+
name: connection_pool
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
84
91
|
- - "~>"
|
85
92
|
- !ruby/object:Gem::Version
|
86
|
-
version: '
|
93
|
+
version: '2.2'
|
94
|
+
type: :development
|
95
|
+
prerelease: false
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - "~>"
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '2.2'
|
87
101
|
description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
|
88
102
|
email:
|
89
103
|
- leandro.ribeiro.moreira@gmail.com
|
@@ -130,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
144
|
- !ruby/object:Gem::Version
|
131
145
|
version: '0'
|
132
146
|
requirements: []
|
133
|
-
rubygems_version: 3.
|
147
|
+
rubygems_version: 3.1.2
|
134
148
|
signing_key:
|
135
149
|
specification_version: 4
|
136
150
|
summary: Distributed lock using Redis written in Ruby.
|