redlock 1.1.0 → 1.2.0

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: d883de4dd36be4eab1fd32417cdc59a99cd4a7e0a68efbbe1a3865a8f9150ea7
4
- data.tar.gz: f732c4b1fb41bb2d9b34917879d6bc893375d9a2d4d527ecfe29d935c3d998a2
3
+ metadata.gz: f27e3a9be9009072c86f2f38a18bdfdcd2f177890aac365d981711dc040acd2e
4
+ data.tar.gz: efedd1080833cdee147afe6427878ad0df7c824e981e4e8a48c563ac68350d05
5
5
  SHA512:
6
- metadata.gz: b05d03ae1e3e6de8b21d9cc2d7c367d03dba26a1b72b06e821a07e81183345a9ea4e32a5a984d941eb6ef5cd628acb446946e9395d37c7e532b16b5e6444b1eb
7
- data.tar.gz: 3066e7c68d4dd14e7e193bb58f29aad00a4624a47aabd4b7b1dfff32318bcaffad7f38910e1b2ab77a0e8572a71f828dc4c83d8af8a3cd13fe95da7e95910a6e
6
+ metadata.gz: aed115da4ab51ae932533d73e2f2f79d7722e6d77471028bb685d89149d6085fd395af368e9a143231b25b680a57c1ad4d236365396974c12416dac1131cabd8
7
+ data.tar.gz: 75b69cd60c2f562b087179dc939c2fb2bbf3514018dd8c302cf4bc2c4cfa4352a1310dd7cfc35042badcd8c86fc5d237d3a6bcafc57385ca2afcc5a2a280c7bf
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redlock (1.1.0)
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 (11.3.0)
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 (~> 11.1, >= 11.1.2)
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
- [![Stories in Ready](https://badge.waffle.io/leandromoreira/redlock-rb.png?label=ready&title=Ready)](https://waffle.io/leandromoreira/redlock-rb)
2
1
  [![Build Status](https://travis-ci.org/leandromoreira/redlock-rb.svg?branch=master)](https://travis-ci.org/leandromoreira/redlock-rb)
3
2
  [![Coverage Status](https://coveralls.io/repos/leandromoreira/redlock-rb/badge.svg?branch=master)](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
4
3
  [![Code Climate](https://codeclimate.com/github/leandromoreira/redlock-rb/badges/gpa.svg)](https://codeclimate.com/github/leandromoreira/redlock-rb)
5
4
  [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
6
5
  [![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
7
6
  [![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
8
- [![Join the chat at https://gitter.im/leandromoreira/redlock-rb](https://badges.gitter.im/Join%20Chat.svg)](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
@@ -3,5 +3,9 @@ require 'redlock/version'
3
3
  module Redlock
4
4
  autoload :Client, 'redlock/client'
5
5
 
6
- LockError = Class.new(StandardError)
6
+ class LockError < StandardError
7
+ def initialize(resource)
8
+ super "failed to acquire lock on '#{resource}'".freeze
9
+ end
10
+ end
7
11
  end
@@ -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, 'failed to acquire lock' unless lock_info
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?(:client)
132
+ if connection.respond_to?(:with)
127
133
  @redis = connection
128
134
  else
129
- @redis = Redis.new(connection)
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((@retry_delay + rand(@retry_jitter)).to_f / 1000) if attempt_number > 0
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'
@@ -1,17 +1,27 @@
1
1
  module Redlock
2
2
  class Client
3
- attr_writer :testing_mode
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 @testing_mode == :bypass
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 @testing_mode == :fail
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 @testing_mode == :bypass
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)
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -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', '~> 11.1', '>= 11.1.2'
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
@@ -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, Redis.new(url: 'redis://localhost:46864'))
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.instance_variable_set(:@redis, Redis.new(url: 'redis://localhost:46864'))
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, Redis.new(url: "redis://#{redis1_host}:#{redis1_port}"))
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(Redlock::LockError)
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.1.0
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: 2019-11-15 00:00:00.000000000 Z
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: '3'
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.0.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.