redlock 1.2.1 → 1.2.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: a91b03cbb845e7b01556262e80e946e2d72980beeebce12d51380d7c57c57fb9
4
- data.tar.gz: 73d38bac32a8003a556d3d8967155d15a8edbe988fc43387174ec506b21e8c99
3
+ metadata.gz: 1870dc8b8577a8e8545fd952fd6c2f1ff3ac1f336bff5f632b3b89eb92eb0069
4
+ data.tar.gz: b5153369a5cd331a4bbc0884efd657692da4b824e0cbe0dd8ed78fdd407d7047
5
5
  SHA512:
6
- metadata.gz: e55aa47e007175e619e22ec2b2ca996add245143290750c79a27fbafc8bb6bd32e5c02b07d9a9752c1ad8cb7f22a10a2b245c9b963c1b4f186427d6c7a987d3a
7
- data.tar.gz: dd61e0d187a5972ecbe09bde3345c1a6174263f06fca6b7d1da3ce8be19fe5bb69ffca4cfc24073705c5b5a40551bb1a65ffba1358c5fbaf37f10ea46a15a929
6
+ metadata.gz: 747aa1155827f513a0612e6610dd754143c3fb65c50aaeca87506830d91c83ec40a89f7fcfb81d22395ea2cc16048cc99136894f0b6a8adb7455d2adae60c244
7
+ data.tar.gz: b033303f287bdbeca822c6b3f0ad829531c5740ad0716f6a6cdeb75370ea9e3874838b761f65ac242b07a07af0b1c0ab955e288059f0953aed3aeb686773a6cc
data/.gitignore CHANGED
@@ -1,5 +1,4 @@
1
1
  *.gem
2
2
  coverage/
3
3
  .bundle/
4
- Gemfile.lock
5
4
  dump.rdb
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ redlock (1.2.2)
5
+ redis (>= 3.0.0, < 5.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ connection_pool (2.2.5)
11
+ coveralls (0.8.23)
12
+ json (>= 1.8, < 3)
13
+ simplecov (~> 0.16.1)
14
+ term-ansicolor (~> 1.3)
15
+ thor (>= 0.19.4, < 2.0)
16
+ tins (~> 1.6)
17
+ diff-lcs (1.4.4)
18
+ docile (1.4.0)
19
+ json (2.3.1)
20
+ rake (13.0.6)
21
+ redis (4.4.0)
22
+ rspec (3.10.0)
23
+ rspec-core (~> 3.10.0)
24
+ rspec-expectations (~> 3.10.0)
25
+ rspec-mocks (~> 3.10.0)
26
+ rspec-core (3.10.1)
27
+ rspec-support (~> 3.10.0)
28
+ rspec-expectations (3.10.1)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.10.0)
31
+ rspec-mocks (3.10.2)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.10.0)
34
+ rspec-support (3.10.2)
35
+ simplecov (0.16.1)
36
+ docile (~> 1.1)
37
+ json (>= 1.8, < 3)
38
+ simplecov-html (~> 0.10.0)
39
+ simplecov-html (0.10.2)
40
+ sync (0.5.0)
41
+ term-ansicolor (1.7.1)
42
+ tins (~> 1.0)
43
+ thor (1.1.0)
44
+ tins (1.29.1)
45
+ sync
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ connection_pool (~> 2.2)
52
+ coveralls (~> 0.8)
53
+ json (~> 2.3.1, >= 2.3.0)
54
+ rake (~> 13.0, >= 11.1.2)
55
+ redlock!
56
+ rspec (~> 3, >= 3.0.0)
57
+
58
+ BUNDLED WITH
59
+ 2.2.22
data/Makefile CHANGED
@@ -7,3 +7,6 @@ build:
7
7
 
8
8
  publish:
9
9
  docker-compose run --rm test gem push `ls -lt *gem | head -n 1 | awk '{ print $$9 }'`
10
+
11
+ updateLock:
12
+ docker-compose run --rm test bundle lock --update
@@ -2,6 +2,8 @@ require 'redis'
2
2
  require 'securerandom'
3
3
 
4
4
  module Redlock
5
+ include Scripts
6
+
5
7
  class Client
6
8
  DEFAULT_REDIS_HOST = ENV["DEFAULT_REDIS_HOST"] || "localhost"
7
9
  DEFAULT_REDIS_PORT = ENV["DEFAULT_REDIS_PORT"] || "6379"
@@ -54,6 +56,9 @@ module Redlock
54
56
  # +resource+:: the resource (or key) string to be locked.
55
57
  # +ttl+:: The time-to-live in ms for the lock.
56
58
  # +options+:: Hash of optional parameters
59
+ # * +retry_count+: see +initialize+
60
+ # * +retry_delay+: see +initialize+
61
+ # * +retry_jitter+: see +initialize+
57
62
  # * +extend+: A lock ("lock_info") to extend.
58
63
  # * +extend_only_if_locked+: Boolean, if +extend+ is given, only acquire lock if currently held
59
64
  # * +extend_only_if_life+: Deprecated, same as +extend_only_if_locked+
@@ -142,27 +147,6 @@ module Redlock
142
147
  private
143
148
 
144
149
  class RedisInstance
145
- UNLOCK_SCRIPT = <<-eos
146
- if redis.call("get",KEYS[1]) == ARGV[1] then
147
- return redis.call("del",KEYS[1])
148
- else
149
- return 0
150
- end
151
- eos
152
-
153
- # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
154
- # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
155
- # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
156
- LOCK_SCRIPT = <<-eos
157
- if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
158
- return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
159
- end
160
- eos
161
-
162
- PTTL_SCRIPT = <<-eos
163
- return { redis.call("get", KEYS[1]), redis.call("pttl", KEYS[1]) }
164
- eos
165
-
166
150
  module ConnectionPoolLike
167
151
  def with
168
152
  yield self
@@ -180,13 +164,11 @@ module Redlock
180
164
  end
181
165
  @redis.extend(ConnectionPoolLike)
182
166
  end
183
-
184
- load_scripts
185
167
  end
186
168
 
187
169
  def lock(resource, val, ttl, allow_new_lock)
188
170
  recover_from_script_flush do
189
- @redis.with { |conn| conn.evalsha @lock_script_sha, keys: [resource], argv: [val, ttl, allow_new_lock] }
171
+ @redis.with { |conn| conn.evalsha Scripts::LOCK_SCRIPT_SHA, keys: [resource], argv: [val, ttl, allow_new_lock] }
190
172
  end
191
173
  rescue Redis::BaseConnectionError
192
174
  false
@@ -194,7 +176,7 @@ module Redlock
194
176
 
195
177
  def unlock(resource, val)
196
178
  recover_from_script_flush do
197
- @redis.with { |conn| conn.evalsha @unlock_script_sha, keys: [resource], argv: [val] }
179
+ @redis.with { |conn| conn.evalsha Scripts::UNLOCK_SCRIPT_SHA, keys: [resource], argv: [val] }
198
180
  end
199
181
  rescue
200
182
  # Nothing to do, unlocking is just a best-effort attempt.
@@ -202,7 +184,7 @@ module Redlock
202
184
 
203
185
  def get_remaining_ttl(resource)
204
186
  recover_from_script_flush do
205
- @redis.with { |conn| conn.evalsha @pttl_script_sha, keys: [resource] }
187
+ @redis.with { |conn| conn.evalsha Scripts::PTTL_SCRIPT_SHA, keys: [resource] }
206
188
  end
207
189
  rescue Redis::BaseConnectionError
208
190
  nil
@@ -211,9 +193,15 @@ module Redlock
211
193
  private
212
194
 
213
195
  def load_scripts
214
- @unlock_script_sha = @redis.with { |conn| conn.script(:load, UNLOCK_SCRIPT) }
215
- @lock_script_sha = @redis.with { |conn| conn.script(:load, LOCK_SCRIPT) }
216
- @pttl_script_sha = @redis.with { |conn| conn.script(:load, PTTL_SCRIPT) }
196
+ scripts = [
197
+ Scripts::UNLOCK_SCRIPT,
198
+ Scripts::LOCK_SCRIPT,
199
+ Scripts::PTTL_SCRIPT
200
+ ]
201
+
202
+ scripts.each do |script|
203
+ @redis.with { |conn| conn.script(:load, script) }
204
+ end
217
205
  end
218
206
 
219
207
  def recover_from_script_flush
@@ -236,11 +224,12 @@ module Redlock
236
224
  end
237
225
 
238
226
  def try_lock_instances(resource, ttl, options)
239
- tries = options[:extend] ? 1 : (@retry_count + 1)
227
+ retry_count = options[:retry_count] || @retry_count
228
+ tries = options[:extend] ? 1 : (retry_count + 1)
240
229
 
241
230
  tries.times do |attempt_number|
242
231
  # Wait a random delay before retrying.
243
- sleep(attempt_retry_delay(attempt_number)) if attempt_number > 0
232
+ sleep(attempt_retry_delay(attempt_number, options)) if attempt_number > 0
244
233
 
245
234
  lock_info = lock_instances(resource, ttl, options)
246
235
  return lock_info if lock_info
@@ -249,15 +238,18 @@ module Redlock
249
238
  false
250
239
  end
251
240
 
252
- def attempt_retry_delay(attempt_number)
241
+ def attempt_retry_delay(attempt_number, options)
242
+ retry_delay = options[:retry_delay] || @retry_delay
243
+ retry_jitter = options[:retry_jitter] || @retry_jitter
244
+
253
245
  retry_delay =
254
- if @retry_delay.respond_to?(:call)
255
- @retry_delay.call(attempt_number)
246
+ if retry_delay.respond_to?(:call)
247
+ retry_delay.call(attempt_number)
256
248
  else
257
- @retry_delay
249
+ retry_delay
258
250
  end
259
251
 
260
- (retry_delay + rand(@retry_jitter)).to_f / 1000
252
+ (retry_delay + rand(retry_jitter)).to_f / 1000
261
253
  end
262
254
 
263
255
  def lock_instances(resource, ttl, options)
@@ -0,0 +1,34 @@
1
+ require 'digest'
2
+
3
+ module Redlock
4
+ module Scripts
5
+ UNLOCK_SCRIPT = <<-eos
6
+ if redis.call("get",KEYS[1]) == ARGV[1] then
7
+ return redis.call("del",KEYS[1])
8
+ else
9
+ return 0
10
+ end
11
+ eos
12
+
13
+ # thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
14
+ # also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
15
+ # and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
16
+ LOCK_SCRIPT = <<-eos
17
+ if (redis.call("exists", KEYS[1]) == 0 and ARGV[3] == "yes") or redis.call("get", KEYS[1]) == ARGV[1] then
18
+ return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
19
+ end
20
+ eos
21
+
22
+ PTTL_SCRIPT = <<-eos
23
+ return { redis.call("get", KEYS[1]), redis.call("pttl", KEYS[1]) }
24
+ eos
25
+
26
+ # We do not want to load the scripts on every Redlock::Client initialization.
27
+ # Hence, we rely on Redis handing out SHA1 hashes of the cached scripts and
28
+ # pre-calculate them instead of loading the scripts unconditionally. If the scripts
29
+ # have not been cached on Redis, `recover_from_script_flush` has our backs.
30
+ UNLOCK_SCRIPT_SHA = Digest::SHA1.hexdigest(UNLOCK_SCRIPT)
31
+ LOCK_SCRIPT_SHA = Digest::SHA1.hexdigest(LOCK_SCRIPT)
32
+ PTTL_SCRIPT_SHA = Digest::SHA1.hexdigest(PTTL_SCRIPT)
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Redlock
2
- VERSION = '1.2.1'
2
+ VERSION = '1.2.2'
3
3
  end
data/lib/redlock.rb CHANGED
@@ -2,6 +2,7 @@ require 'redlock/version'
2
2
 
3
3
  module Redlock
4
4
  autoload :Client, 'redlock/client'
5
+ autoload :Scripts, 'redlock/scripts'
5
6
 
6
7
  class LockError < StandardError
7
8
  def initialize(resource)
data/redlock.gemspec CHANGED
@@ -15,7 +15,6 @@ Gem::Specification.new do |spec|
15
15
  spec.license = 'BSD-2-Clause'
16
16
 
17
17
  spec.files = `git ls-files -z`.split("\x0")
18
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
19
  spec.require_paths = ['lib']
21
20
 
data/spec/client_spec.rb CHANGED
@@ -44,6 +44,15 @@ RSpec.describe Redlock::Client do
44
44
  expect(resource_key).to_not be_lockable(lock_manager, ttl)
45
45
  lock_manager.unlock(lock_info)
46
46
  end
47
+
48
+ it 'does not load scripts' do
49
+ redis_client.script(:flush)
50
+
51
+ pool = ConnectionPool.new { Redis.new(url: "redis://#{redis1_host}:#{redis1_port}") }
52
+ redlock = Redlock::Client.new([pool])
53
+
54
+ expect(redis_client.info["number_of_cached_scripts"]).to eq("0")
55
+ end
47
56
  end
48
57
 
49
58
  describe 'lock' do
@@ -195,6 +204,41 @@ RSpec.describe Redlock::Client do
195
204
  lock_manager.lock(resource_key, ttl)
196
205
  lock_manager.unlock(another_lock_info)
197
206
  end
207
+
208
+ context 'when retry_count is given' do
209
+ it 'prioritizes the retry_count in option and tries up to \'retry_count\' + 1 times' do
210
+ retry_count = 1
211
+ expect(retry_count).not_to eq(lock_manager_opts[:retry_count])
212
+ expect(lock_manager).to receive(:lock_instances).exactly(retry_count + 1).times.and_return(false)
213
+ lock_manager.lock(resource_key, ttl, retry_count: retry_count)
214
+ end
215
+ end
216
+
217
+ context 'when retry_delay is given' do
218
+ it 'prioritizes the retry_delay in option and sleeps at least the specified retry_delay in milliseconds' do
219
+ retry_delay = 300
220
+ expect(retry_delay > described_class::DEFAULT_RETRY_DELAY).to eq(true)
221
+ expected_minimum = retry_delay
222
+
223
+ expect(lock_manager).to receive(:sleep) do |sleep|
224
+ expect(sleep).to satisfy { |value| value >= expected_minimum / 1000.to_f }
225
+ end.at_least(:once)
226
+ lock_manager.lock(resource_key, ttl, retry_delay: retry_delay)
227
+ end
228
+ end
229
+
230
+ context 'when retry_jitter is given' do
231
+ it 'prioritizes the retry_jitter in option and sleeps a maximum of retry_delay + retry_jitter in milliseconds' do
232
+ retry_jitter = 60
233
+ expect(retry_jitter > described_class::DEFAULT_RETRY_JITTER).to eq(true)
234
+
235
+ expected_maximum = described_class::DEFAULT_RETRY_DELAY + retry_jitter
236
+ expect(lock_manager).to receive(:sleep) do |sleep|
237
+ expect(sleep).to satisfy { |value| value < expected_maximum / 1000.to_f }
238
+ end.at_least(:once)
239
+ lock_manager.lock(resource_key, ttl, retry_jitter: retry_jitter)
240
+ end
241
+ end
198
242
  end
199
243
 
200
244
  context 'when a server goes away' 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.2.1
4
+ version: 1.2.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: 2020-12-08 00:00:00.000000000 Z
11
+ date: 2021-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -130,6 +130,7 @@ files:
130
130
  - ".travis.yml"
131
131
  - CONTRIBUTORS
132
132
  - Gemfile
133
+ - Gemfile.lock
133
134
  - LICENSE
134
135
  - Makefile
135
136
  - README.md
@@ -138,6 +139,7 @@ files:
138
139
  - docker-compose.yml
139
140
  - lib/redlock.rb
140
141
  - lib/redlock/client.rb
142
+ - lib/redlock/scripts.rb
141
143
  - lib/redlock/testing.rb
142
144
  - lib/redlock/version.rb
143
145
  - redlock.gemspec
@@ -163,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
165
  - !ruby/object:Gem::Version
164
166
  version: '0'
165
167
  requirements: []
166
- rubygems_version: 3.1.4
168
+ rubygems_version: 3.2.22
167
169
  signing_key:
168
170
  specification_version: 4
169
171
  summary: Distributed lock using Redis written in Ruby.