redlock 2.0.0 → 2.0.2
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/CHANGELOG.md +40 -0
- data/README.md +2 -1
- data/lib/redlock/client.rb +35 -4
- data/lib/redlock/version.rb +1 -1
- data/redlock.gemspec +1 -1
- data/spec/client_spec.rb +69 -10
- data/spec/testing_spec.rb +3 -3
- metadata +14 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8bdda4faebf87c2d1fb0026a3142497d5ae14092340bce6e35e46f51635e5c6a
|
4
|
+
data.tar.gz: a9f5636898a2a3ece98b95a286bd88f417e68d119b1f1ad60bd75b5d1da94baf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b10ec10f00ce9282d604b1d0c8a2e845d8d95a6684a5a235d6d39b598efa6324725c4d088e8016b11d473b0195a1e29a451e79fad115f04f6b217493be297cf
|
7
|
+
data.tar.gz: e1d54bb8bb8a108e0989d1c11baa44cbfc4c971029fb23819d000758b42739698342ef803a826990757c888d113fb8a49663df1f3812603965c5498361596f86
|
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"
|
@@ -168,13 +177,17 @@ module Redlock
|
|
168
177
|
|
169
178
|
def lock(resource, val, ttl, allow_new_lock)
|
170
179
|
recover_from_script_flush do
|
171
|
-
@redis.
|
180
|
+
@redis.with { |conn|
|
181
|
+
conn.call('EVALSHA', Scripts::LOCK_SCRIPT_SHA, 1, resource, val, ttl, allow_new_lock)
|
182
|
+
}
|
172
183
|
end
|
173
184
|
end
|
174
185
|
|
175
186
|
def unlock(resource, val)
|
176
187
|
recover_from_script_flush do
|
177
|
-
@redis.
|
188
|
+
@redis.with { |conn|
|
189
|
+
conn.call('EVALSHA', Scripts::UNLOCK_SCRIPT_SHA, 1, resource, val)
|
190
|
+
}
|
178
191
|
end
|
179
192
|
rescue
|
180
193
|
# Nothing to do, unlocking is just a best-effort attempt.
|
@@ -182,7 +195,9 @@ module Redlock
|
|
182
195
|
|
183
196
|
def get_remaining_ttl(resource)
|
184
197
|
recover_from_script_flush do
|
185
|
-
@redis.
|
198
|
+
@redis.with { |conn|
|
199
|
+
conn.call('EVALSHA', Scripts::PTTL_SCRIPT_SHA, 1, resource)
|
200
|
+
}
|
186
201
|
end
|
187
202
|
rescue RedisClient::ConnectionError
|
188
203
|
nil
|
@@ -226,6 +241,7 @@ module Redlock
|
|
226
241
|
def try_lock_instances(resource, ttl, options)
|
227
242
|
retry_count = options[:retry_count] || @retry_count
|
228
243
|
tries = options[:extend] ? 1 : (retry_count + 1)
|
244
|
+
last_error = nil
|
229
245
|
|
230
246
|
tries.times do |attempt_number|
|
231
247
|
# Wait a random delay before retrying.
|
@@ -233,8 +249,12 @@ module Redlock
|
|
233
249
|
|
234
250
|
lock_info = lock_instances(resource, ttl, options)
|
235
251
|
return lock_info if lock_info
|
252
|
+
rescue => e
|
253
|
+
last_error = e
|
236
254
|
end
|
237
255
|
|
256
|
+
raise last_error if last_error
|
257
|
+
|
238
258
|
false
|
239
259
|
end
|
240
260
|
|
@@ -255,9 +275,15 @@ module Redlock
|
|
255
275
|
def lock_instances(resource, ttl, options)
|
256
276
|
value = (options[:extend] || { value: SecureRandom.uuid })[:value]
|
257
277
|
allow_new_lock = options[:extend_only_if_locked] ? 'no' : 'yes'
|
278
|
+
errors = []
|
258
279
|
|
259
280
|
locked, time_elapsed = timed do
|
260
|
-
@servers.
|
281
|
+
@servers.count do |s|
|
282
|
+
s.lock(resource, value, ttl, allow_new_lock)
|
283
|
+
rescue => e
|
284
|
+
errors << e
|
285
|
+
false
|
286
|
+
end
|
261
287
|
end
|
262
288
|
|
263
289
|
validity = ttl - time_elapsed - drift(ttl)
|
@@ -266,6 +292,11 @@ module Redlock
|
|
266
292
|
{ validity: validity, resource: resource, value: value }
|
267
293
|
else
|
268
294
|
@servers.each { |s| s.unlock(resource, value) }
|
295
|
+
|
296
|
+
if errors.size >= @quorum
|
297
|
+
raise LockAcquisitionError.new('Too many Redis errors prevented lock acquisition', errors)
|
298
|
+
end
|
299
|
+
|
269
300
|
false
|
270
301
|
end
|
271
302
|
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'
|
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
@@ -5,7 +5,21 @@ require 'connection_pool'
|
|
5
5
|
RSpec.describe Redlock::Client do
|
6
6
|
# It is recommended to have at least 3 servers in production
|
7
7
|
let(:lock_manager_opts) { { retry_count: 3 } }
|
8
|
-
let(:
|
8
|
+
let(:redis_urls_or_clients) {
|
9
|
+
urls = Redlock::Client::DEFAULT_REDIS_URLS
|
10
|
+
if rand(0..1).zero?
|
11
|
+
RSpec.configuration.reporter.message "variant: client urls"
|
12
|
+
urls
|
13
|
+
else
|
14
|
+
RSpec.configuration.reporter.message "variant: client objects"
|
15
|
+
urls.map {|url|
|
16
|
+
ConnectionPool.new { RedisClient.new(url: url) }
|
17
|
+
}
|
18
|
+
end
|
19
|
+
}
|
20
|
+
let(:lock_manager) {
|
21
|
+
Redlock::Client.new(redis_urls_or_clients, lock_manager_opts)
|
22
|
+
}
|
9
23
|
let(:redis_client) { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
10
24
|
let(:resource_key) { SecureRandom.hex(3) }
|
11
25
|
let(:ttl) { 1000 }
|
@@ -37,9 +51,10 @@ RSpec.describe Redlock::Client do
|
|
37
51
|
|
38
52
|
it 'accepts ConnectionPool objects' do
|
39
53
|
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
40
|
-
|
54
|
+
_redlock = Redlock::Client.new([pool])
|
41
55
|
|
42
56
|
lock_info = lock_manager.lock(resource_key, ttl)
|
57
|
+
expect(lock_info).to be_a(Hash)
|
43
58
|
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
44
59
|
lock_manager.unlock(lock_info)
|
45
60
|
end
|
@@ -48,7 +63,7 @@ RSpec.describe Redlock::Client do
|
|
48
63
|
redis_client.call('SCRIPT', 'FLUSH')
|
49
64
|
|
50
65
|
pool = ConnectionPool.new { RedisClient.new(url: "redis://#{redis1_host}:#{redis1_port}") }
|
51
|
-
|
66
|
+
_redlock = Redlock::Client.new([pool])
|
52
67
|
|
53
68
|
raw_info = redis_client.call('INFO')
|
54
69
|
number_of_cached_scripts = raw_info[/number_of_cached_scripts\:\d+/].split(':').last
|
@@ -61,6 +76,38 @@ RSpec.describe Redlock::Client do
|
|
61
76
|
context 'when lock is available' do
|
62
77
|
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
63
78
|
|
79
|
+
context 'when redis connection error occurs' do
|
80
|
+
let(:servers_with_quorum) {
|
81
|
+
[
|
82
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
83
|
+
"redis://#{redis2_host}:#{redis2_port}",
|
84
|
+
unreachable_redis
|
85
|
+
]
|
86
|
+
}
|
87
|
+
|
88
|
+
let(:servers_without_quorum) {
|
89
|
+
[
|
90
|
+
"redis://#{redis1_host}:#{redis1_port}",
|
91
|
+
unreachable_redis,
|
92
|
+
unreachable_redis
|
93
|
+
]
|
94
|
+
}
|
95
|
+
|
96
|
+
it 'locks if majority of redis instances are available' do
|
97
|
+
redlock = Redlock::Client.new(servers_with_quorum)
|
98
|
+
|
99
|
+
expect(redlock.lock(resource_key, ttl)).to be_truthy
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'fails to acquire a lock if majority of Redis instances are not available' do
|
103
|
+
redlock = Redlock::Client.new(servers_without_quorum)
|
104
|
+
|
105
|
+
expect {
|
106
|
+
redlock.lock(resource_key, ttl)
|
107
|
+
}.to raise_error(Redlock::LockAcquisitionError)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
64
111
|
it 'locks' do
|
65
112
|
@lock_info = lock_manager.lock(resource_key, ttl)
|
66
113
|
|
@@ -197,7 +244,7 @@ RSpec.describe Redlock::Client do
|
|
197
244
|
2000
|
198
245
|
end
|
199
246
|
|
200
|
-
lock_manager = Redlock::Client.new(
|
247
|
+
lock_manager = Redlock::Client.new(redis_urls_or_clients, retry_count: 1, retry_delay: retry_delay)
|
201
248
|
another_lock_info = lock_manager.lock(resource_key, ttl)
|
202
249
|
|
203
250
|
expect(lock_manager).to receive(:sleep) do |sleep|
|
@@ -251,7 +298,10 @@ RSpec.describe Redlock::Client do
|
|
251
298
|
|
252
299
|
expect {
|
253
300
|
lock_manager.lock(resource_key, ttl)
|
254
|
-
}.to raise_error(
|
301
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
302
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
303
|
+
expect(e.errors.count).to eq 1
|
304
|
+
end
|
255
305
|
end
|
256
306
|
end
|
257
307
|
|
@@ -263,7 +313,10 @@ RSpec.describe Redlock::Client do
|
|
263
313
|
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
264
314
|
expect {
|
265
315
|
lock_manager.lock(resource_key, ttl)
|
266
|
-
}.to raise_error(
|
316
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
317
|
+
expect(e.errors[0]).to be_a(RedisClient::CannotConnectError)
|
318
|
+
expect(e.errors.count).to eq 1
|
319
|
+
end
|
267
320
|
redis_instance.instance_variable_set(:@redis, old_redis)
|
268
321
|
expect(lock_manager.lock(resource_key, ttl)).to be_truthy
|
269
322
|
end
|
@@ -272,7 +325,9 @@ RSpec.describe Redlock::Client do
|
|
272
325
|
context 'when script cache has been flushed' do
|
273
326
|
before(:each) do
|
274
327
|
@manipulated_instance = lock_manager.instance_variable_get(:@servers).first
|
275
|
-
@manipulated_instance.instance_variable_get(:@redis).
|
328
|
+
@manipulated_instance.instance_variable_get(:@redis).with { |conn|
|
329
|
+
conn.call('SCRIPT', 'FLUSH')
|
330
|
+
}
|
276
331
|
end
|
277
332
|
|
278
333
|
it 'does not raise a RedisClient::CommandError: NOSCRIPT error' do
|
@@ -291,11 +346,15 @@ RSpec.describe Redlock::Client do
|
|
291
346
|
# This time we do not pass it through to Redis, in order to simulate a passing
|
292
347
|
# call to LOAD SCRIPT followed by another NOSCRIPT error. Imagine someone
|
293
348
|
# repeatedly calling SCRIPT FLUSH on our Redis instance.
|
294
|
-
expect(@manipulated_instance).to receive(:load_scripts)
|
349
|
+
expect(@manipulated_instance).to receive(:load_scripts).exactly(8).times
|
295
350
|
|
296
351
|
expect {
|
297
352
|
lock_manager.lock(resource_key, ttl)
|
298
|
-
}.to raise_error(
|
353
|
+
}.to raise_error(Redlock::LockAcquisitionError) do |e|
|
354
|
+
expect(e.errors[0]).to be_a(RedisClient::CommandError)
|
355
|
+
expect(e.errors[0].message).to match(/NOSCRIPT/)
|
356
|
+
expect(e.errors.count).to eq 1
|
357
|
+
end
|
299
358
|
end
|
300
359
|
end
|
301
360
|
|
@@ -475,7 +534,7 @@ RSpec.describe Redlock::Client do
|
|
475
534
|
|
476
535
|
# Replace redis with unreachable instance
|
477
536
|
redis_instance = lock_manager.instance_variable_get(:@servers).first
|
478
|
-
|
537
|
+
_old_redis = redis_instance.instance_variable_get(:@redis)
|
479
538
|
redis_instance.instance_variable_set(:@redis, unreachable_redis)
|
480
539
|
|
481
540
|
expect {
|
data/spec/testing_spec.rb
CHANGED
@@ -11,7 +11,7 @@ RSpec.describe Redlock::Client do
|
|
11
11
|
describe '(testing mode)' do
|
12
12
|
describe 'try_lock_instances' do
|
13
13
|
context 'when testing with bypass mode' do
|
14
|
-
before {
|
14
|
+
before { Redlock::Client.testing_mode = :bypass }
|
15
15
|
|
16
16
|
it 'bypasses the redis servers' do
|
17
17
|
expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
|
@@ -22,7 +22,7 @@ RSpec.describe Redlock::Client do
|
|
22
22
|
end
|
23
23
|
|
24
24
|
context 'when testing with fail mode' do
|
25
|
-
before {
|
25
|
+
before { Redlock::Client.testing_mode = :fail }
|
26
26
|
|
27
27
|
it 'fails' do
|
28
28
|
expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
|
@@ -33,7 +33,7 @@ RSpec.describe Redlock::Client do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
context 'when testing is disabled' do
|
36
|
-
before {
|
36
|
+
before { Redlock::Client.testing_mode = nil }
|
37
37
|
|
38
38
|
it 'works as usual' do
|
39
39
|
expect(lock_manager).to receive(:try_lock_instances_without_testing)
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
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.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Leandro Moreira
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.14.1
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 0.14.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: connection_pool
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,22 +96,22 @@ dependencies:
|
|
96
96
|
name: rspec
|
97
97
|
requirement: !ruby/object:Gem::Requirement
|
98
98
|
requirements:
|
99
|
-
- - ">="
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
version: 3.0.0
|
102
99
|
- - "~>"
|
103
100
|
- !ruby/object:Gem::Version
|
104
101
|
version: '3'
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 3.0.0
|
105
105
|
type: :development
|
106
106
|
prerelease: false
|
107
107
|
version_requirements: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
|
-
- - ">="
|
110
|
-
- !ruby/object:Gem::Version
|
111
|
-
version: 3.0.0
|
112
109
|
- - "~>"
|
113
110
|
- !ruby/object:Gem::Version
|
114
111
|
version: '3'
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: 3.0.0
|
115
115
|
description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
|
116
116
|
email:
|
117
117
|
- leandro.ribeiro.moreira@gmail.com
|
@@ -122,6 +122,7 @@ files:
|
|
122
122
|
- ".github/workflows/ci.yml"
|
123
123
|
- ".gitignore"
|
124
124
|
- ".rspec"
|
125
|
+
- CHANGELOG.md
|
125
126
|
- CONTRIBUTORS
|
126
127
|
- Gemfile
|
127
128
|
- LICENSE
|
@@ -158,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
159
|
- !ruby/object:Gem::Version
|
159
160
|
version: '0'
|
160
161
|
requirements: []
|
161
|
-
rubygems_version: 3.
|
162
|
+
rubygems_version: 3.3.7
|
162
163
|
signing_key:
|
163
164
|
specification_version: 4
|
164
165
|
summary: Distributed lock using Redis written in Ruby.
|