redlock 1.2.1 → 1.3.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: 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