redlock 2.0.0 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 679d1c44fcda7a2eaa70bdfe38c30e4ee79e417bef0b3ce064aceaf0a9cc0bbb
4
- data.tar.gz: 48b6a41c4cc27b8ff8878647dd494a6b87e3b17e540b75d8699e32d4ee982aa5
3
+ metadata.gz: 8bdda4faebf87c2d1fb0026a3142497d5ae14092340bce6e35e46f51635e5c6a
4
+ data.tar.gz: a9f5636898a2a3ece98b95a286bd88f417e68d119b1f1ad60bd75b5d1da94baf
5
5
  SHA512:
6
- metadata.gz: a31815a7da2fa6f5c3af35734294cdb34fdfba19dc12eaf25abe950db65be1356afc397ff29a73823d267d99d0069f86651385c1e262da6ced7541345212abda
7
- data.tar.gz: cd4e41fe60732180b4b5f3d848715954074f8a063c1513c89e73a37311d5d4c0d8fbe1bbd92b063955324929f07bf59232040de104bfeb14d18837d297910d59
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
- Redlock works with Redis versions 6.0 or later.
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
 
@@ -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.call('EVALSHA', Scripts::LOCK_SCRIPT_SHA, 1, resource, val, ttl, allow_new_lock)
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.call('EVALSHA', Scripts::UNLOCK_SCRIPT_SHA, 1, resource, val)
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.call('EVALSHA', Scripts::PTTL_SCRIPT_SHA, 1, resource)
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.select { |s| s.lock resource, value, ttl, allow_new_lock }.size
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
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '2.0.0'
2
+ VERSION = '2.0.2'
3
3
  end
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(:lock_manager) { Redlock::Client.new(Redlock::Client::DEFAULT_REDIS_URLS, lock_manager_opts) }
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
- redlock = Redlock::Client.new([pool])
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
- redlock = Redlock::Client.new([pool])
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(Redlock::Client::DEFAULT_REDIS_URLS, retry_count: 1, retry_delay: retry_delay)
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(RedisClient::CannotConnectError)
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(RedisClient::CannotConnectError)
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).call('SCRIPT', 'FLUSH')
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(/NOSCRIPT/)
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
- old_redis = redis_instance.instance_variable_get(:@redis)
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 { lock_manager.testing_mode = :bypass }
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 { lock_manager.testing_mode = :fail }
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 { lock_manager.testing_mode = nil }
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.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-02-09 00:00:00.000000000 Z
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: '0'
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: '0'
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.0.3.1
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.