redlock 1.2.1 → 1.3.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: a91b03cbb845e7b01556262e80e946e2d72980beeebce12d51380d7c57c57fb9
4
- data.tar.gz: 73d38bac32a8003a556d3d8967155d15a8edbe988fc43387174ec506b21e8c99
3
+ metadata.gz: 7de9b6b9d70bce2578e474bdfe8dfa15c22bb57e0c876ac7127d826a5b6ba9b4
4
+ data.tar.gz: c294d219107aa4a8682ba4ff303e150497ea61b2dd4abb71d3cac15e78c218bd
5
5
  SHA512:
6
- metadata.gz: e55aa47e007175e619e22ec2b2ca996add245143290750c79a27fbafc8bb6bd32e5c02b07d9a9752c1ad8cb7f22a10a2b245c9b963c1b4f186427d6c7a987d3a
7
- data.tar.gz: dd61e0d187a5972ecbe09bde3345c1a6174263f06fca6b7d1da3ce8be19fe5bb69ffca4cfc24073705c5b5a40551bb1a65ffba1358c5fbaf37f10ea46a15a929
6
+ metadata.gz: b0eeead2b1307f12487603d72a7a5e00d6aba1d697a5327ac484a4fa4cc00575e0fcedd43a38aa34ee93da7322550630ad3cec158a5ef0fcf98849033c90ec85
7
+ data.tar.gz: 712d3c40ab8c4278f4649f373bfcb6ca4eeb2361461a48ad42d17f44a9d1becba460b82ed99f6b035232698267ad5046831230960b47171d9fa09d9ba0d4b692
@@ -0,0 +1,31 @@
1
+ name: Ruby CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ ruby-version: [3.1, "3.0", "2.7", "2.6", "2.5", "ruby-head"]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Set up Ruby ${{ matrix.ruby-version }}
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby-version }}
24
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
25
+ - name: Start Redis
26
+ uses: supercharge/redis-github-action@1.2.0
27
+ with:
28
+ redis-version: 6
29
+ - name: Run tests
30
+ run: bundle exec rspec
31
+
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,62 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ redlock (1.3.2)
5
+ redis (>= 3.0.0, < 6.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ connection_pool (2.3.0)
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.5.0)
18
+ docile (1.4.0)
19
+ json (2.3.1)
20
+ rake (13.0.6)
21
+ redis (5.0.5)
22
+ redis-client (>= 0.9.0)
23
+ redis-client (0.10.0)
24
+ connection_pool
25
+ rspec (3.11.0)
26
+ rspec-core (~> 3.11.0)
27
+ rspec-expectations (~> 3.11.0)
28
+ rspec-mocks (~> 3.11.0)
29
+ rspec-core (3.11.0)
30
+ rspec-support (~> 3.11.0)
31
+ rspec-expectations (3.11.1)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.11.0)
34
+ rspec-mocks (3.11.1)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.11.0)
37
+ rspec-support (3.11.1)
38
+ simplecov (0.16.1)
39
+ docile (~> 1.1)
40
+ json (>= 1.8, < 3)
41
+ simplecov-html (~> 0.10.0)
42
+ simplecov-html (0.10.2)
43
+ sync (0.5.0)
44
+ term-ansicolor (1.7.1)
45
+ tins (~> 1.0)
46
+ thor (1.2.1)
47
+ tins (1.31.1)
48
+ sync
49
+
50
+ PLATFORMS
51
+ ruby
52
+
53
+ DEPENDENCIES
54
+ connection_pool (~> 2.2)
55
+ coveralls (~> 0.8)
56
+ json (~> 2.3.1, >= 2.3.0)
57
+ rake (~> 13.0, >= 11.1.2)
58
+ redlock!
59
+ rspec (~> 3, >= 3.0.0)
60
+
61
+ BUNDLED WITH
62
+ 2.3.7
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
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
- [![Build Status](https://travis-ci.org/leandromoreira/redlock-rb.svg?branch=master)](https://travis-ci.org/leandromoreira/redlock-rb)
1
+ [![Build Status](https://github.com/leandromoreira/redlock-rb/actions/workflows/ci.yml/badge.svg)](https://github.com/leandromoreira/redlock-rb/actions/workflows/ci.yml)
2
2
  [![Coverage Status](https://coveralls.io/repos/leandromoreira/redlock-rb/badge.svg?branch=master)](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
3
3
  [![Code Climate](https://codeclimate.com/github/leandromoreira/redlock-rb/badges/gpa.svg)](https://codeclimate.com/github/leandromoreira/redlock-rb)
4
4
  [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
5
- [![security](https://hakiri.io/github/leandromoreira/redlock-rb/master.svg)](https://hakiri.io/github/leandromoreira/redlock-rb/master)
6
5
  [![Inline docs](http://inch-ci.org/github/leandromoreira/redlock-rb.svg?branch=master)](http://inch-ci.org/github/leandromoreira/redlock-rb)
7
6
 
8
7
 
@@ -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,21 +164,17 @@ 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
- rescue Redis::BaseConnectionError
192
- false
193
173
  end
194
174
 
195
175
  def unlock(resource, val)
196
176
  recover_from_script_flush do
197
- @redis.with { |conn| conn.evalsha @unlock_script_sha, keys: [resource], argv: [val] }
177
+ @redis.with { |conn| conn.evalsha Scripts::UNLOCK_SCRIPT_SHA, keys: [resource], argv: [val] }
198
178
  end
199
179
  rescue
200
180
  # Nothing to do, unlocking is just a best-effort attempt.
@@ -202,7 +182,7 @@ module Redlock
202
182
 
203
183
  def get_remaining_ttl(resource)
204
184
  recover_from_script_flush do
205
- @redis.with { |conn| conn.evalsha @pttl_script_sha, keys: [resource] }
185
+ @redis.with { |conn| conn.evalsha Scripts::PTTL_SCRIPT_SHA, keys: [resource] }
206
186
  end
207
187
  rescue Redis::BaseConnectionError
208
188
  nil
@@ -211,9 +191,15 @@ module Redlock
211
191
  private
212
192
 
213
193
  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) }
194
+ scripts = [
195
+ Scripts::UNLOCK_SCRIPT,
196
+ Scripts::LOCK_SCRIPT,
197
+ Scripts::PTTL_SCRIPT
198
+ ]
199
+
200
+ scripts.each do |script|
201
+ @redis.with { |conn| conn.script(:load, script) }
202
+ end
217
203
  end
218
204
 
219
205
  def recover_from_script_flush
@@ -236,11 +222,12 @@ module Redlock
236
222
  end
237
223
 
238
224
  def try_lock_instances(resource, ttl, options)
239
- tries = options[:extend] ? 1 : (@retry_count + 1)
225
+ retry_count = options[:retry_count] || @retry_count
226
+ tries = options[:extend] ? 1 : (retry_count + 1)
240
227
 
241
228
  tries.times do |attempt_number|
242
229
  # Wait a random delay before retrying.
243
- sleep(attempt_retry_delay(attempt_number)) if attempt_number > 0
230
+ sleep(attempt_retry_delay(attempt_number, options)) if attempt_number > 0
244
231
 
245
232
  lock_info = lock_instances(resource, ttl, options)
246
233
  return lock_info if lock_info
@@ -249,15 +236,18 @@ module Redlock
249
236
  false
250
237
  end
251
238
 
252
- def attempt_retry_delay(attempt_number)
239
+ def attempt_retry_delay(attempt_number, options)
240
+ retry_delay = options[:retry_delay] || @retry_delay
241
+ retry_jitter = options[:retry_jitter] || @retry_jitter
242
+
253
243
  retry_delay =
254
- if @retry_delay.respond_to?(:call)
255
- @retry_delay.call(attempt_number)
244
+ if retry_delay.respond_to?(:call)
245
+ retry_delay.call(attempt_number)
256
246
  else
257
- @retry_delay
247
+ retry_delay
258
248
  end
259
249
 
260
- (retry_delay + rand(@retry_jitter)).to_f / 1000
250
+ (retry_delay + rand(retry_jitter)).to_f / 1000
261
251
  end
262
252
 
263
253
  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.3.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,11 +15,10 @@ 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
 
22
- spec.add_dependency 'redis', '>= 3.0.0', '< 5.0'
21
+ spec.add_dependency 'redis', '>= 3.0.0', '< 6.0'
23
22
 
24
23
  spec.add_development_dependency 'connection_pool', '~> 2.2'
25
24
  spec.add_development_dependency 'coveralls', '~> 0.8'
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,17 +204,52 @@ 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
201
- it 'does not raise an error on connection issues' do
202
- # We re-route the lock manager to a (hopefully) non-existent Redis URL.
245
+ it 'raises an error on connection issues' do
246
+ # Set lock manager to a (hopefully) non-existent Redis URL to test error
203
247
  redis_instance = lock_manager.instance_variable_get(:@servers).first
204
248
  redis_instance.instance_variable_set(:@redis, unreachable_redis)
205
249
 
206
250
  expect {
207
- expect(lock_manager.lock(resource_key, ttl)).to be_falsey
208
- }.to_not raise_error
251
+ lock_manager.lock(resource_key, ttl)
252
+ }.to raise_error(Redis::CannotConnectError)
209
253
  end
210
254
  end
211
255
 
@@ -215,7 +259,9 @@ RSpec.describe Redlock::Client do
215
259
  redis_instance = lock_manager.instance_variable_get(:@servers).first
216
260
  old_redis = redis_instance.instance_variable_get(:@redis)
217
261
  redis_instance.instance_variable_set(:@redis, unreachable_redis)
218
- expect(lock_manager.lock(resource_key, ttl)).to be_falsey
262
+ expect {
263
+ lock_manager.lock(resource_key, ttl)
264
+ }.to raise_error(Redis::CannotConnectError)
219
265
  redis_instance.instance_variable_set(:@redis, old_redis)
220
266
  expect(lock_manager.lock(resource_key, ttl)).to be_truthy
221
267
  end
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.3.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: 2022-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: 3.0.0
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '5.0'
22
+ version: '6.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: 3.0.0
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '5.0'
32
+ version: '6.0'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: connection_pool
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -125,11 +125,12 @@ executables: []
125
125
  extensions: []
126
126
  extra_rdoc_files: []
127
127
  files:
128
+ - ".github/workflows/ci.yml"
128
129
  - ".gitignore"
129
130
  - ".rspec"
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.3.7
167
169
  signing_key:
168
170
  specification_version: 4
169
171
  summary: Distributed lock using Redis written in Ruby.
data/.travis.yml DELETED
@@ -1,23 +0,0 @@
1
- language: ruby
2
- cache: bundler
3
- sudo: false
4
-
5
- rvm:
6
- - 2.4.9
7
- - 2.5.7
8
- - 2.6.5
9
- - 2.7.0
10
- - ruby-head
11
-
12
- before_install:
13
- - yes | gem update --system
14
- - gem install bundler -v "~> 2.0"
15
-
16
- script: bundle exec rspec spec
17
-
18
- jobs:
19
- allow_failures:
20
- - rvm: ruby-head
21
-
22
- services:
23
- - redis-server