redlock 2.0.1 → 2.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +40 -0
- data/README.md +2 -1
- data/lib/redlock/client.rb +50 -2
- data/lib/redlock/version.rb +1 -1
- data/redlock.gemspec +1 -1
- data/spec/client_spec.rb +56 -4
- metadata +15 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb95cd103dd1856b7c75459b121bd5905d2884f520850552552bdb1c39f7502b
|
4
|
+
data.tar.gz: 165945b2aca8e081ad2704fa9eb699bd2e08e75f973fc7ae802163eed88dce8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79ba3e5ab3dc1704b2af22efb3f038a43b8b9fd773492ba8e6d8f8e0b162cdd4df3671e2267ac80ff3e1f1a3d0edb3b3cc29430637f6d3efa146fd1f6d4fd100
|
7
|
+
data.tar.gz: '0929d107e6ffb002498a26b5afe0d59e41eacc7265ade1402584b1c25a52509ddbedd40eb24acfacfefa718f343ffe9f055adff4f3f7b390e61971543dea4451'
|
data/.github/workflows/ci.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
# Change Log
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
6
|
+
and this project adheres to [Semantic Versioning](http://semver.org/).
|
7
|
+
|
8
|
+
## [Unreleased] - yyyy-mm-dd
|
9
|
+
|
10
|
+
Here we write upgrading notes for brands. It's a team effort to make them as
|
11
|
+
straightforward as possible.
|
12
|
+
|
13
|
+
### Added
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
|
17
|
+
### Fixed
|
18
|
+
|
19
|
+
|
20
|
+
## [2.0.1] - 2023-02-14
|
21
|
+
|
22
|
+
### Added
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
|
28
|
+
* always treat redis instance as pool-like #125
|
29
|
+
|
30
|
+
## [2.0.0] - 2023-02-09
|
31
|
+
|
32
|
+
### Added
|
33
|
+
|
34
|
+
* support for redis >= 6.0
|
35
|
+
|
36
|
+
### Changed
|
37
|
+
|
38
|
+
* **BREAKING**: The library now only works with `RedisClient` instance.
|
39
|
+
|
40
|
+
### Fixed
|
data/README.md
CHANGED
@@ -15,7 +15,8 @@ This is an implementation of a proposed [distributed lock algorithm with Redis](
|
|
15
15
|
|
16
16
|
## Compatibility
|
17
17
|
|
18
|
-
|
18
|
+
* It works with Redis server versions 6.0 or later.
|
19
|
+
* Redlock >= 2.0 only works with [`RedisClient`](https://github.com/redis-rb/redis-client) client instance.
|
19
20
|
|
20
21
|
## Installation
|
21
22
|
|
data/lib/redlock/client.rb
CHANGED
@@ -4,6 +4,15 @@ require 'securerandom'
|
|
4
4
|
module Redlock
|
5
5
|
include Scripts
|
6
6
|
|
7
|
+
class LockAcquisitionError < StandardError
|
8
|
+
attr_reader :errors
|
9
|
+
|
10
|
+
def initialize(message, errors)
|
11
|
+
super(message)
|
12
|
+
@errors = errors
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
7
16
|
class Client
|
8
17
|
DEFAULT_REDIS_HOST = ENV["DEFAULT_REDIS_HOST"] || "localhost"
|
9
18
|
DEFAULT_REDIS_PORT = ENV["DEFAULT_REDIS_PORT"] || "6379"
|
@@ -160,12 +169,35 @@ module Redlock
|
|
160
169
|
if connection.respond_to?(:client)
|
161
170
|
@redis = connection
|
162
171
|
else
|
163
|
-
@redis =
|
172
|
+
@redis = initialize_client(connection)
|
164
173
|
end
|
165
174
|
@redis.extend(ConnectionPoolLike)
|
166
175
|
end
|
167
176
|
end
|
168
177
|
|
178
|
+
def initialize_client(options)
|
179
|
+
if options.key?(:sentinels)
|
180
|
+
if url = options.delete(:url)
|
181
|
+
uri = URI.parse(url)
|
182
|
+
if !options.key?(:name) && uri.host
|
183
|
+
options[:name] = uri.host
|
184
|
+
end
|
185
|
+
|
186
|
+
if !options.key?(:password) && uri.password && !uri.password.empty?
|
187
|
+
options[:password] = uri.password
|
188
|
+
end
|
189
|
+
|
190
|
+
if !options.key?(:username) && uri.user && !uri.user.empty?
|
191
|
+
options[:username] = uri.user
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
RedisClient.sentinel(**options).new_client
|
196
|
+
else
|
197
|
+
RedisClient.config(**options).new_client
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
169
201
|
def lock(resource, val, ttl, allow_new_lock)
|
170
202
|
recover_from_script_flush do
|
171
203
|
@redis.with { |conn|
|
@@ -232,6 +264,7 @@ module Redlock
|
|
232
264
|
def try_lock_instances(resource, ttl, options)
|
233
265
|
retry_count = options[:retry_count] || @retry_count
|
234
266
|
tries = options[:extend] ? 1 : (retry_count + 1)
|
267
|
+
last_error = nil
|
235
268
|
|
236
269
|
tries.times do |attempt_number|
|
237
270
|
# Wait a random delay before retrying.
|
@@ -239,8 +272,12 @@ module Redlock
|
|
239
272
|
|
240
273
|
lock_info = lock_instances(resource, ttl, options)
|
241
274
|
return lock_info if lock_info
|
275
|
+
rescue => e
|
276
|
+
last_error = e
|
242
277
|
end
|
243
278
|
|
279
|
+
raise last_error if last_error
|
280
|
+
|
244
281
|
false
|
245
282
|
end
|
246
283
|
|
@@ -261,9 +298,15 @@ module Redlock
|
|
261
298
|
def lock_instances(resource, ttl, options)
|
262
299
|
value = (options[:extend] || { value: SecureRandom.uuid })[:value]
|
263
300
|
allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
|
301
|
+
errors = []
|
264
302
|
|
265
303
|
locked, time_elapsed = timed do
|
266
|
-
@servers.
|
304
|
+
@servers.count do |s|
|
305
|
+
s.lock(resource, value, ttl, allow_new_lock)
|
306
|
+
rescue => e
|
307
|
+
errors << e
|
308
|
+
false
|
309
|
+
end
|
267
310
|
end
|
268
311
|
|
269
312
|
validity = ttl - time_elapsed - drift(ttl)
|
@@ -272,6 +315,11 @@ module Redlock
|
|
272
315
|
{ validity: validity, resource: resource, value: value }
|
273
316
|
else
|
274
317
|
@servers.each { |s| s.unlock(resource, value) }
|
318
|
+
|
319
|
+
if errors.size >= @quorum
|
320
|
+
raise LockAcquisitionError.new('Too many Redis errors prevented lock acquisition', errors)
|
321
|
+
end
|
322
|
+
|
275
323
|
false
|
276
324
|
end
|
277
325
|
end
|
data/lib/redlock/version.rb
CHANGED
data/redlock.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.add_dependency 'redis-client'
|
21
|
+
spec.add_dependency 'redis-client', '>= 0.14.1', '< 1.0.0'
|
22
22
|
|
23
23
|
spec.add_development_dependency 'connection_pool', '~> 2.2'
|
24
24
|
spec.add_development_dependency 'coveralls', '~> 0.8'
|
data/spec/client_spec.rb
CHANGED
@@ -59,6 +59,16 @@ RSpec.describe Redlock::Client do
|
|
59
59
|
lock_manager.unlock(lock_info)
|
60
60
|
end
|
61
61
|
|
62
|
+
it 'accepts Configuration hashes' do
|
63
|
+
config = { url: "redis://#{redis1_host}:#{redis1_port}" }
|
64
|
+
_redlock = Redlock::Client.new([config])
|
65
|
+
|
66
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
67
|
+
expect(lock_info).to be_a(Hash)
|
68
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
69
|
+
lock_manager.unlock(lock_info)
|
70
|
+
end
|
71
|
+
|
62
72
|
it 'does not load scripts' do
|
63
73
|
redis_client.call('SCRIPT', 'FLUSH')
|
64
74
|
|
@@ -76,6 +86,38 @@ RSpec.describe Redlock::Client do
|
|
76
86
|
context 'when lock is available' do
|
77
87
|
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
78
88
|
|
89
|
+
context 'when redis connection error occurs' do
|
90
|
+
let(:servers_with_quorum) {
|
91
|
+
[
|
92
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
93
|
+
"redis://#{redis2_host}:#{redis2_port}",
|
94
|
+
unreachable_redis
|
95
|
+
]
|
96
|
+
}
|
97
|
+
|
98
|
+
let(:servers_without_quorum) {
|
99
|
+
[
|
100
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
101
|
+
unreachable_redis,
|
102
|
+
unreachable_redis
|
103
|
+
]
|
104
|
+
}
|
105
|
+
|
106
|
+
it 'locks if majority of redis instances are available' do
|
107
|
+
redlock = Redlock::Client.new(servers_with_quorum)
|
108
|
+
|
109
|
+
expect(redlock.lock(resource_key, ttl)).to be_truthy
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'fails to acquire a lock if majority of Redis instances are not available' do
|
113
|
+
redlock = Redlock::Client.new(servers_without_quorum)
|
114
|
+
|
115
|
+
expect {
|
116
|
+
redlock.lock(resource_key, ttl)
|
117
|
+
}.to raise_error(Redlock::LockAcquisitionError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
79
121
|
it 'locks' do
|
80
122
|
@lock_info = lock_manager.lock(resource_key, ttl)
|
81
123
|
|
@@ -266,7 +308,10 @@ RSpec.describe Redlock::Client do
|
|
266
308
|
|
267
309
|
expect {
|
268
310
|
lock_manager.lock(resource_key, ttl)
|
269
|
-
}.to raise_error(
|
311
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
312
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
313
|
+
expect(e.errors.count).to eq 1
|
314
|
+
end
|
270
315
|
end
|
271
316
|
end
|
272
317
|
|
@@ -278,7 +323,10 @@ RSpec.describe Redlock::Client do
|
|
278
323
|
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
279
324
|
expect {
|
280
325
|
lock_manager.lock(resource_key, ttl)
|
281
|
-
}.to raise_error(
|
326
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
327
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
328
|
+
expect(e.errors.count).to eq 1
|
329
|
+
end
|
282
330
|
redis_instance.instance_variable_set(:@redis, old_redis)
|
283
331
|
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
|
284
332
|
end
|
@@ -308,11 +356,15 @@ RSpec.describe Redlock::Client do
|
|
308
356
|
# This time we do not pass it through to Redis, in order to simulate a passing
|
309
357
|
# call to LOAD SCRIPT followed by another NOSCRIPT error. Imagine someone
|
310
358
|
# repeatedly calling SCRIPT FLUSH on our Redis instance.
|
311
|
-
expect(@manipulated_instance).to receive(:load_scripts)
|
359
|
+
expect(@manipulated_instance).to receive(:load_scripts).exactly(8).times
|
312
360
|
|
313
361
|
expect {
|
314
362
|
lock_manager.lock(resource_key, ttl)
|
315
|
-
}.to raise_error(
|
363
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
364
|
+
expect(e.errors[0]).to be_a(RedisClient::CommandError)
|
365
|
+
expect(e.errors[0].message).to match(/NOSCRIPT/)
|
366
|
+
expect(e.errors.count).to eq 1
|
367
|
+
end
|
316
368
|
end
|
317
369
|
end
|
318
370
|
|
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: 2.0.
|
4
|
+
version: 2.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro Moreira
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -16,14 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.14.1
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.0.0
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
29
|
+
version: 0.14.1
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.0
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: connection_pool
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +128,7 @@ files:
|
|
122
128
|
- ".github/workflows/ci.yml"
|
123
129
|
- ".gitignore"
|
124
130
|
- ".rspec"
|
131
|
+
- CHANGELOG.md
|
125
132
|
- CONTRIBUTORS
|
126
133
|
- Gemfile
|
127
134
|
- LICENSE
|
@@ -143,7 +150,7 @@ homepage: https://github.com/leandromoreira/redlock-rb
|
|
143
150
|
licenses:
|
144
151
|
- BSD-2-Clause
|
145
152
|
metadata: {}
|
146
|
-
post_install_message:
|
153
|
+
post_install_message:
|
147
154
|
rdoc_options: []
|
148
155
|
require_paths:
|
149
156
|
- lib
|
@@ -158,8 +165,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
165
|
- !ruby/object:Gem::Version
|
159
166
|
version: '0'
|
160
167
|
requirements: []
|
161
|
-
rubygems_version: 3.
|
162
|
-
signing_key:
|
168
|
+
rubygems_version: 3.4.10
|
169
|
+
signing_key:
|
163
170
|
specification_version: 4
|
164
171
|
summary: Distributed lock using Redis written in Ruby.
|
165
172
|
test_files:
|