seamusabshere-redlock 1.0.0
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 +7 -0
- data/.gitignore +2 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CONTRIBUTORS +11 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +57 -0
- data/LICENSE +26 -0
- data/README.md +100 -0
- data/Rakefile +2 -0
- data/lib/redlock.rb +5 -0
- data/lib/redlock/client.rb +143 -0
- data/lib/redlock/testing.rb +27 -0
- data/lib/redlock/version.rb +3 -0
- data/redlock.gemspec +27 -0
- data/spec/client_spec.rb +135 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/testing_spec.rb +45 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a3db3c911a17f8b605fa6d1792916d28cb46dfcd
|
4
|
+
data.tar.gz: a7d53070405f730965e3966ebd3a0e4cf70297a4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6cee08cdc8d900bbdfbf23187995d3a777d4b55f43d7b1e718463ba0a5021ac64c1991edf6dd603f51c2eeef3f7c0605a6179ff882802b0b381568df8603b0b9
|
7
|
+
data.tar.gz: 7b555aec308f81d40f6d34bbe706ea1c1a3353dbf6dd607e36ae68e6f87b6d28168cbcf39729b7f6819556ba17ea70af08887a6381a3364144e83202db7013b1
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CONTRIBUTORS
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# This is the official list of people who have contributed code to
|
2
|
+
# redlock.
|
3
|
+
# You can update this list using the following command:
|
4
|
+
#
|
5
|
+
# % git shortlog -se | awk '{$1=""; print $0}' | sed -e 's/^ //'
|
6
|
+
#
|
7
|
+
# Please keep this file sorted, and group users with multiple emails.
|
8
|
+
|
9
|
+
Leandro Moreira <leandro.ribeiro.moreira@gmail.com>
|
10
|
+
Malte Rohde
|
11
|
+
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
redlock (0.1.1)
|
5
|
+
redis (~> 3, >= 3.0.5)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
coveralls (0.7.11)
|
11
|
+
multi_json (~> 1.10)
|
12
|
+
rest-client (>= 1.6.8, < 2)
|
13
|
+
simplecov (~> 0.9.1)
|
14
|
+
term-ansicolor (~> 1.3)
|
15
|
+
thor (~> 0.19.1)
|
16
|
+
diff-lcs (1.2.5)
|
17
|
+
docile (1.1.5)
|
18
|
+
mime-types (2.4.3)
|
19
|
+
multi_json (1.11.0)
|
20
|
+
netrc (0.10.3)
|
21
|
+
rake (10.4.2)
|
22
|
+
redis (3.2.1)
|
23
|
+
rest-client (1.7.3)
|
24
|
+
mime-types (>= 1.16, < 3.0)
|
25
|
+
netrc (~> 0.7)
|
26
|
+
rspec (3.2.0)
|
27
|
+
rspec-core (~> 3.2.0)
|
28
|
+
rspec-expectations (~> 3.2.0)
|
29
|
+
rspec-mocks (~> 3.2.0)
|
30
|
+
rspec-core (3.2.2)
|
31
|
+
rspec-support (~> 3.2.0)
|
32
|
+
rspec-expectations (3.2.0)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.2.0)
|
35
|
+
rspec-mocks (3.2.1)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.2.0)
|
38
|
+
rspec-support (3.2.2)
|
39
|
+
simplecov (0.9.2)
|
40
|
+
docile (~> 1.1.0)
|
41
|
+
multi_json (~> 1.0)
|
42
|
+
simplecov-html (~> 0.9.0)
|
43
|
+
simplecov-html (0.9.0)
|
44
|
+
term-ansicolor (1.3.0)
|
45
|
+
tins (~> 1.0)
|
46
|
+
thor (0.19.1)
|
47
|
+
tins (1.3.5)
|
48
|
+
|
49
|
+
PLATFORMS
|
50
|
+
ruby
|
51
|
+
|
52
|
+
DEPENDENCIES
|
53
|
+
bundler (~> 1.7)
|
54
|
+
coveralls
|
55
|
+
rake (~> 10.0)
|
56
|
+
redlock!
|
57
|
+
rspec (~> 3.1)
|
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Copyright (c) 2014-2015, Salvatore Sanfilippo <antirez at gmail dot com>
|
2
|
+
Copyright (c) 2014-2015, Leandro Moreira <leandro dot ribeiro dot moreira at gmail dot com>
|
3
|
+
Copyright (c) 2015, Malte Rohde <malte dot rohde at flavoursys dot com>
|
4
|
+
|
5
|
+
All rights reserved.
|
6
|
+
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
9
|
+
|
10
|
+
* Redistributions of source code must retain the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer.
|
12
|
+
|
13
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
15
|
+
and/or other materials provided with the distribution.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
18
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
19
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
21
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
22
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
23
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
24
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
25
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
26
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
[](https://waffle.io/leandromoreira/redlock-rb)
|
2
|
+
[](https://travis-ci.org/leandromoreira/redlock-rb)
|
3
|
+
[](https://coveralls.io/r/leandromoreira/redlock-rb?branch=master)
|
4
|
+
[](https://codeclimate.com/github/leandromoreira/redlock-rb)
|
5
|
+
[](https://gemnasium.com/leandromoreira/redlock-rb)
|
6
|
+
[](http://badge.fury.io/rb/redlock)
|
7
|
+
[](https://hakiri.io/github/leandromoreira/redlock-rb/master)
|
8
|
+
[](http://inch-ci.org/github/leandromoreira/redlock-rb)
|
9
|
+
[](https://gitter.im/leandromoreira/redlock-rb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
10
|
+
|
11
|
+
[](https://codeship.com/projects/901ff180-c1ad-0132-1a88-3eb2295b72b3/status?branch=master)
|
12
|
+
|
13
|
+
|
14
|
+
# Redlock - A ruby distributed lock using redis.
|
15
|
+
|
16
|
+
# TEMPORARY GEM TO RELEASE `extend` FUNCTIONALITY https://github.com/leandromoreira/redlock-rb/pull/20
|
17
|
+
|
18
|
+
> Distributed locks are a very useful primitive in many environments where different processes require to operate with shared resources in a mutually exclusive way.
|
19
|
+
>
|
20
|
+
> There are a number of libraries and blog posts describing how to implement a DLM (Distributed Lock Manager) with Redis, but every library uses a different approach, and many use a simple approach with lower guarantees compared to what can be achieved with slightly more complex designs.
|
21
|
+
|
22
|
+
This is an implementation of a proposed [distributed lock algorithm with Redis](http://redis.io/topics/distlock). It started as a fork from [antirez implementation.](https://github.com/antirez/redlock-rb)
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'redlock'
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install redlock
|
39
|
+
|
40
|
+
## Documentation
|
41
|
+
|
42
|
+
[RubyDoc](http://www.rubydoc.info/gems/redlock/frames)
|
43
|
+
|
44
|
+
## Usage example
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# Locking
|
48
|
+
lock_manager = Redlock::Client.new([ "redis://127.0.0.1:7777", "redis://127.0.0.1:7778", "redis://127.0.0.1:7779" ])
|
49
|
+
first_try_lock_info = lock_manager.lock("resource_key", 2000)
|
50
|
+
second_try_lock_info = lock_manager.lock("resource_key", 2000)
|
51
|
+
|
52
|
+
# it prints lock info {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
|
53
|
+
p first_try_lock_info
|
54
|
+
# it prints false
|
55
|
+
p second_try_lock_info
|
56
|
+
|
57
|
+
# Unlocking
|
58
|
+
lock_manager.unlock(first_try_lock_info)
|
59
|
+
second_try_lock_info = lock_manager.lock("resource_key", 2000)
|
60
|
+
|
61
|
+
# now it prints lock info
|
62
|
+
p second_try_lock_info
|
63
|
+
```
|
64
|
+
|
65
|
+
Redlock works seamlessly with [redis sentinel](http://redis.io/topics/sentinel), which is supported in redis 3.2+. It also allows clients to set any other arbitrary options on the Redis connection, e.g. password, driver, and more.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
servers = [ 'redis://localhost:6379', Redis.new(:url => 'redis://someotherhost:6379') ]
|
69
|
+
redlock = Redlock::Client.new(servers)
|
70
|
+
```
|
71
|
+
|
72
|
+
There's also a block version that automatically unlocks the lock:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
lock_manager.lock("resource_key", 2000) do |locked|
|
76
|
+
if locked
|
77
|
+
# critical code
|
78
|
+
else
|
79
|
+
# error handling
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
## Run tests
|
85
|
+
|
86
|
+
Make sure you have at least 1 redis instances up.
|
87
|
+
|
88
|
+
$ rspec
|
89
|
+
|
90
|
+
## Disclaimer
|
91
|
+
|
92
|
+
This code implements an algorithm which is currently a proposal, it was not formally analyzed. Make sure to understand how it works before using it in your production environments. You can see discussion about this approach at [reddit](http://www.reddit.com/r/programming/comments/2nt0nq/distributed_lock_using_redis_implemented_in_ruby/).
|
93
|
+
|
94
|
+
## Contributing
|
95
|
+
|
96
|
+
1. [Fork it](https://github.com/leandromoreira/redlock-rb/fork)
|
97
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
98
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
99
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
100
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/lib/redlock.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Redlock
|
5
|
+
class Client
|
6
|
+
DEFAULT_REDIS_URLS = ['redis://localhost:6379']
|
7
|
+
DEFAULT_REDIS_TIMEOUT = 0.1
|
8
|
+
DEFAULT_RETRY_COUNT = 3
|
9
|
+
DEFAULT_RETRY_DELAY = 200
|
10
|
+
CLOCK_DRIFT_FACTOR = 0.01
|
11
|
+
|
12
|
+
# Create a distributed lock manager implementing redlock algorithm.
|
13
|
+
# Params:
|
14
|
+
# +servers+:: The array of redis connection URLs or Redis connection instances. Or a mix of both.
|
15
|
+
# +options+:: You can override the default value for `retry_count` and `retry_delay`.
|
16
|
+
# * `retry_count` being how many times it'll try to lock a resource (default: 3)
|
17
|
+
# * `retry_delay` being how many ms to sleep before try to lock again (default: 200)
|
18
|
+
# * `redis_timeout` being how the Redis timeout will be set in seconds (default: 0.1)
|
19
|
+
def initialize(servers = DEFAULT_REDIS_URLS, options = {})
|
20
|
+
redis_timeout = options[:redis_timeout] || DEFAULT_REDIS_TIMEOUT
|
21
|
+
@servers = servers.map do |server|
|
22
|
+
if server.is_a?(String)
|
23
|
+
RedisInstance.new(url: server, timeout: redis_timeout)
|
24
|
+
else
|
25
|
+
RedisInstance.new(server)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
@quorum = servers.length / 2 + 1
|
29
|
+
@retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
|
30
|
+
@retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
|
31
|
+
end
|
32
|
+
|
33
|
+
# Locks a resource for a given time.
|
34
|
+
# Params:
|
35
|
+
# +resource+:: the resource (or key) string to be locked.
|
36
|
+
# +ttl+:: The time-to-live in ms for the lock.
|
37
|
+
# +extend+: A lock ("lock_info") to extend.
|
38
|
+
# +block+:: an optional block that automatically unlocks the lock.
|
39
|
+
def lock(resource, ttl, extend: nil, &block)
|
40
|
+
lock_info = try_lock_instances(resource, ttl, extend)
|
41
|
+
|
42
|
+
if block_given?
|
43
|
+
begin
|
44
|
+
yield lock_info
|
45
|
+
!!lock_info
|
46
|
+
ensure
|
47
|
+
unlock(lock_info) if lock_info
|
48
|
+
end
|
49
|
+
else
|
50
|
+
lock_info
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Unlocks a resource.
|
55
|
+
# Params:
|
56
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource.
|
57
|
+
def unlock(lock_info)
|
58
|
+
@servers.each { |s| s.unlock(lock_info[:resource], lock_info[:value]) }
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
class RedisInstance
|
64
|
+
UNLOCK_SCRIPT = <<-eos
|
65
|
+
if redis.call("get",KEYS[1]) == ARGV[1] then
|
66
|
+
return redis.call("del",KEYS[1])
|
67
|
+
else
|
68
|
+
return 0
|
69
|
+
end
|
70
|
+
eos
|
71
|
+
# thanks to https://github.com/sbertrang/redis-distlock/blob/master/lib/Redis/DistLock.pm
|
72
|
+
# also https://github.com/sbertrang/redis-distlock/issues/2 which proposes the value-checking
|
73
|
+
# and @maltoe for https://github.com/leandromoreira/redlock-rb/pull/20#discussion_r38903633
|
74
|
+
LOCK_SCRIPT = <<-eos
|
75
|
+
if redis.call("exists", KEYS[1]) == 0 or redis.call("get", KEYS[1]) == ARGV[1] then
|
76
|
+
return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
|
77
|
+
end
|
78
|
+
eos
|
79
|
+
|
80
|
+
def initialize(connection)
|
81
|
+
if connection.respond_to?(:client)
|
82
|
+
@redis = connection
|
83
|
+
else
|
84
|
+
@redis = Redis.new(connection)
|
85
|
+
end
|
86
|
+
|
87
|
+
@unlock_script_sha = @redis.script(:load, UNLOCK_SCRIPT)
|
88
|
+
@lock_script_sha = @redis.script(:load, LOCK_SCRIPT)
|
89
|
+
end
|
90
|
+
|
91
|
+
def lock(resource, val, ttl)
|
92
|
+
@redis.evalsha(@lock_script_sha, keys: [resource], argv: [val, ttl])
|
93
|
+
end
|
94
|
+
|
95
|
+
def unlock(resource, val)
|
96
|
+
@redis.evalsha(@unlock_script_sha, keys: [resource], argv: [val])
|
97
|
+
rescue
|
98
|
+
# Nothing to do, unlocking is just a best-effort attempt.
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def try_lock_instances(resource, ttl, extend)
|
103
|
+
@retry_count.times do
|
104
|
+
lock_info = lock_instances(resource, ttl, extend)
|
105
|
+
return lock_info if lock_info
|
106
|
+
|
107
|
+
# Wait a random delay before retrying
|
108
|
+
sleep(rand(@retry_delay).to_f / 1000)
|
109
|
+
end
|
110
|
+
|
111
|
+
false
|
112
|
+
end
|
113
|
+
|
114
|
+
def lock_instances(resource, ttl, extend)
|
115
|
+
value = extend ? extend.fetch(:value) : SecureRandom.uuid
|
116
|
+
|
117
|
+
locked, time_elapsed = timed do
|
118
|
+
@servers.select { |s| s.lock(resource, value, ttl) }.size
|
119
|
+
end
|
120
|
+
|
121
|
+
validity = ttl - time_elapsed - drift(ttl)
|
122
|
+
|
123
|
+
if locked >= @quorum && validity >= 0
|
124
|
+
{ validity: validity, resource: resource, value: value }
|
125
|
+
else
|
126
|
+
@servers.each { |s| s.unlock(resource, value) }
|
127
|
+
false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def drift(ttl)
|
132
|
+
# Add 2 milliseconds to the drift to account for Redis expires
|
133
|
+
# precision, which is 1 millisecond, plus 1 millisecond min drift
|
134
|
+
# for small TTLs.
|
135
|
+
drift = (ttl * CLOCK_DRIFT_FACTOR).to_i + 2
|
136
|
+
end
|
137
|
+
|
138
|
+
def timed
|
139
|
+
start_time = (Time.now.to_f * 1000).to_i
|
140
|
+
[yield, (Time.now.to_f * 1000).to_i - start_time]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Redlock
|
2
|
+
class Client
|
3
|
+
attr_writer :testing_mode
|
4
|
+
|
5
|
+
alias_method :try_lock_instances_without_testing, :try_lock_instances
|
6
|
+
|
7
|
+
def try_lock_instances(resource, ttl, extend)
|
8
|
+
if @testing_mode == :bypass
|
9
|
+
{
|
10
|
+
validity: ttl,
|
11
|
+
resource: resource,
|
12
|
+
value: SecureRandom.uuid
|
13
|
+
}
|
14
|
+
elsif @testing_mode == :fail
|
15
|
+
false
|
16
|
+
else
|
17
|
+
try_lock_instances_without_testing resource, ttl, extend
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method :unlock_without_testing, :unlock
|
22
|
+
|
23
|
+
def unlock(lock_info)
|
24
|
+
unlock_without_testing lock_info unless @testing_mode == :bypass
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/redlock.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'redlock/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "seamusabshere-redlock"
|
8
|
+
spec.version = Redlock::VERSION
|
9
|
+
spec.authors = ["Leandro Moreira"]
|
10
|
+
spec.email = ["leandro.ribeiro.moreira@gmail.com"]
|
11
|
+
spec.summary = %q{(temporary gem) Distributed lock using Redis written in Ruby.}
|
12
|
+
spec.description = %q{(temporary gem) Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.}
|
13
|
+
spec.homepage = "https://github.com/leandromoreira/redlock-rb"
|
14
|
+
spec.license = 'BSD-2-Clause'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'redis', '~> 3', '>= 3.0.5'
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "coveralls"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.1"
|
27
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
RSpec.describe Redlock::Client do
|
5
|
+
# It is recommended to have at least 3 servers in production
|
6
|
+
let(:lock_manager) { Redlock::Client.new }
|
7
|
+
let(:resource_key) { SecureRandom.hex(3) }
|
8
|
+
let(:ttl) { 1000 }
|
9
|
+
|
10
|
+
describe 'initialize' do
|
11
|
+
it 'accepts both redis URLs and Redis objects' do
|
12
|
+
servers = [ 'redis://localhost:6379', Redis.new(url: 'redis://127.0.0.1:6379') ]
|
13
|
+
redlock = Redlock::Client.new(servers)
|
14
|
+
|
15
|
+
redlock_servers = redlock.instance_variable_get(:@servers).map do |s|
|
16
|
+
s.instance_variable_get(:@redis).client.host
|
17
|
+
end
|
18
|
+
|
19
|
+
expect(redlock_servers).to match_array(%w{ localhost 127.0.0.1 })
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'lock' do
|
24
|
+
context 'when lock is available' do
|
25
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
26
|
+
|
27
|
+
it 'locks' do
|
28
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
29
|
+
|
30
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns lock information' do
|
34
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
35
|
+
|
36
|
+
expect(@lock_info).to be_lock_info_for(resource_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'can extend its own lock' do
|
40
|
+
my_lock_info = lock_manager.lock(resource_key, ttl)
|
41
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: my_lock_info)
|
42
|
+
expect(@lock_info).to be_lock_info_for(resource_key)
|
43
|
+
expect(@lock_info[:value]).to eq(my_lock_info[:value])
|
44
|
+
end
|
45
|
+
|
46
|
+
it "sets the given value when trying to extend a non-existent lock" do
|
47
|
+
@lock_info = lock_manager.lock(resource_key, ttl, extend: {value: 'hello world'})
|
48
|
+
expect(@lock_info).to be_lock_info_for(resource_key)
|
49
|
+
expect(@lock_info[:value]).to eq('hello world') # really we should test what's in redis
|
50
|
+
end
|
51
|
+
|
52
|
+
it "doesn't extend lock by default" do
|
53
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
54
|
+
second_attempt = lock_manager.lock(resource_key, ttl)
|
55
|
+
expect(second_attempt).to eq(false)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when lock is not available' do
|
60
|
+
before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
|
61
|
+
after { lock_manager.unlock(@another_lock_info) }
|
62
|
+
|
63
|
+
it 'returns false' do
|
64
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
65
|
+
|
66
|
+
expect(lock_info).to eql(false)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "can't extend somebody else's lock" do
|
70
|
+
yet_another_lock_info = @another_lock_info.merge value: 'gibberish'
|
71
|
+
lock_info = lock_manager.lock(resource_key, ttl, extend: yet_another_lock_info)
|
72
|
+
expect(lock_info).to eql(false)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'block syntax' do
|
77
|
+
context 'when lock is available' do
|
78
|
+
it 'locks' do
|
79
|
+
lock_manager.lock(resource_key, ttl) do |_|
|
80
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'passes lock information as block argument' do
|
85
|
+
lock_manager.lock(resource_key, ttl) do |lock_info|
|
86
|
+
expect(lock_info).to be_lock_info_for(resource_key)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'returns true' do
|
91
|
+
rv = lock_manager.lock(resource_key, ttl) {}
|
92
|
+
expect(rv).to eql(true)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'automatically unlocks' do
|
96
|
+
lock_manager.lock(resource_key, ttl) {}
|
97
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'automatically unlocks when block raises exception' do
|
101
|
+
lock_manager.lock(resource_key, ttl) { fail } rescue nil
|
102
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when lock is not available' do
|
107
|
+
before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
|
108
|
+
after { lock_manager.unlock(@another_lock_info) }
|
109
|
+
|
110
|
+
it 'passes false as block argument' do
|
111
|
+
lock_manager.lock(resource_key, ttl) do |lock_info|
|
112
|
+
expect(lock_info).to eql(false)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'returns false' do
|
117
|
+
rv = lock_manager.lock(resource_key, ttl) {}
|
118
|
+
expect(rv).to eql(false)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe 'unlock' do
|
125
|
+
before { @lock_info = lock_manager.lock(resource_key, ttl) }
|
126
|
+
|
127
|
+
it 'unlocks' do
|
128
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
129
|
+
|
130
|
+
lock_manager.unlock(@lock_info)
|
131
|
+
|
132
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'coveralls'
|
3
|
+
Coveralls.wear!
|
4
|
+
require 'redlock'
|
5
|
+
|
6
|
+
LOCK_INFO_KEYS = %i{validity resource value}
|
7
|
+
|
8
|
+
RSpec::Matchers.define :be_lock_info_for do |resource|
|
9
|
+
def correct_type?(actual)
|
10
|
+
actual.is_a?(Hash)
|
11
|
+
end
|
12
|
+
|
13
|
+
def correct_layout?(actual)
|
14
|
+
((LOCK_INFO_KEYS | actual.keys) - (LOCK_INFO_KEYS & actual.keys)).empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def correct_resource?(actual, resource)
|
18
|
+
actual[:resource] == resource
|
19
|
+
end
|
20
|
+
|
21
|
+
match do |actual|
|
22
|
+
correct_type?(actual) && correct_layout?(actual) && correct_resource?(actual, resource)
|
23
|
+
end
|
24
|
+
|
25
|
+
failure_message do |actual|
|
26
|
+
"expected that #{actual} would be lock information for #{expected}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
RSpec::Matchers.define :be_lockable do |lock_manager, ttl|
|
31
|
+
match do |resource_key|
|
32
|
+
begin
|
33
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
34
|
+
lock_info != false
|
35
|
+
ensure
|
36
|
+
lock_manager.unlock(lock_info) if lock_info
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
failure_message do |resource_key|
|
41
|
+
"expected that #{resource_key} would be lockable"
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
require 'redlock/testing'
|
5
|
+
|
6
|
+
RSpec.describe Redlock::Client do
|
7
|
+
let(:lock_manager) { Redlock::Client.new }
|
8
|
+
let(:resource_key) { SecureRandom.hex(3) }
|
9
|
+
let(:ttl) { 1000 }
|
10
|
+
|
11
|
+
describe '(testing mode)' do
|
12
|
+
describe 'try_lock_instances' do
|
13
|
+
context 'when testing with bypass mode' do
|
14
|
+
before { lock_manager.testing_mode = :bypass }
|
15
|
+
|
16
|
+
it 'bypasses the redis servers' do
|
17
|
+
expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
|
18
|
+
lock_manager.lock(resource_key, ttl) do |lock_info|
|
19
|
+
expect(lock_info).to be_lock_info_for(resource_key)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when testing with fail mode' do
|
25
|
+
before { lock_manager.testing_mode = :fail }
|
26
|
+
|
27
|
+
it 'fails' do
|
28
|
+
expect(lock_manager).to_not receive(:try_lock_instances_without_testing)
|
29
|
+
lock_manager.lock(resource_key, ttl) do |lock_info|
|
30
|
+
expect(lock_info).to eql(false)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when testing is disabled' do
|
36
|
+
before { lock_manager.testing_mode = nil }
|
37
|
+
|
38
|
+
it 'works as usual' do
|
39
|
+
expect(lock_manager).to receive(:try_lock_instances_without_testing)
|
40
|
+
lock_manager.lock(resource_key, ttl) { |lock_info| }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: seamusabshere-redlock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Leandro Moreira
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-09-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.0.5
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 3.0.5
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: bundler
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.7'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.7'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: coveralls
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rake
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '10.0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '10.0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.1'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.1'
|
89
|
+
description: "(temporary gem) Distributed lock using Redis written in Ruby. Highly
|
90
|
+
inspired by https://github.com/antirez/redlock-rb."
|
91
|
+
email:
|
92
|
+
- leandro.ribeiro.moreira@gmail.com
|
93
|
+
executables: []
|
94
|
+
extensions: []
|
95
|
+
extra_rdoc_files: []
|
96
|
+
files:
|
97
|
+
- ".gitignore"
|
98
|
+
- ".rspec"
|
99
|
+
- ".travis.yml"
|
100
|
+
- CONTRIBUTORS
|
101
|
+
- Gemfile
|
102
|
+
- Gemfile.lock
|
103
|
+
- LICENSE
|
104
|
+
- README.md
|
105
|
+
- Rakefile
|
106
|
+
- lib/redlock.rb
|
107
|
+
- lib/redlock/client.rb
|
108
|
+
- lib/redlock/testing.rb
|
109
|
+
- lib/redlock/version.rb
|
110
|
+
- redlock.gemspec
|
111
|
+
- spec/client_spec.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
- spec/testing_spec.rb
|
114
|
+
homepage: https://github.com/leandromoreira/redlock-rb
|
115
|
+
licenses:
|
116
|
+
- BSD-2-Clause
|
117
|
+
metadata: {}
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 2.2.2
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: "(temporary gem) Distributed lock using Redis written in Ruby."
|
138
|
+
test_files:
|
139
|
+
- spec/client_spec.rb
|
140
|
+
- spec/spec_helper.rb
|
141
|
+
- spec/testing_spec.rb
|
142
|
+
has_rdoc:
|