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 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.