redlock 0.0.1 → 0.0.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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +8 -0
- data/Gemfile.lock +26 -1
- data/README.md +23 -7
- data/lib/redlock/client.rb +31 -13
- data/lib/redlock/version.rb +1 -1
- data/redlock.gemspec +3 -0
- data/spec/client_spec.rb +85 -13
- data/spec/spec_helper.rb +41 -0
- metadata +48 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89b482115278216ae9e620ce8f7430bf4fe36715
|
4
|
+
data.tar.gz: 75f2da7d8d167fea72815ca9e4ea3242425dfd9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33c9ef53d6833e461ac6c805748fcf20923f29e2fde681c684992cd2ff2fe607d84b5e17b95515331470bce0aa39a7662e5f5dc7a83941a17c9b647072145742
|
7
|
+
data.tar.gz: 068c9b13201e315d9060ed6e646f1e19a6371a0892ece0d9f2fd719d45dee65aa622324a25d06a290e5e56638079f91da58d97fb2b214eea0c2230c185cc6ca4
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
@@ -2,12 +2,27 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
redlock (0.0.1)
|
5
|
+
redis (~> 3, >= 3.0.5)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
8
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)
|
9
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)
|
10
21
|
rake (10.3.2)
|
22
|
+
redis (3.2.1)
|
23
|
+
rest-client (1.7.3)
|
24
|
+
mime-types (>= 1.16, < 3.0)
|
25
|
+
netrc (~> 0.7)
|
11
26
|
rspec (3.1.0)
|
12
27
|
rspec-core (~> 3.1.0)
|
13
28
|
rspec-expectations (~> 3.1.0)
|
@@ -20,12 +35,22 @@ GEM
|
|
20
35
|
rspec-mocks (3.1.3)
|
21
36
|
rspec-support (~> 3.1.0)
|
22
37
|
rspec-support (3.1.2)
|
38
|
+
simplecov (0.9.2)
|
39
|
+
docile (~> 1.1.0)
|
40
|
+
multi_json (~> 1.0)
|
41
|
+
simplecov-html (~> 0.9.0)
|
42
|
+
simplecov-html (0.9.0)
|
43
|
+
term-ansicolor (1.3.0)
|
44
|
+
tins (~> 1.0)
|
45
|
+
thor (0.19.1)
|
46
|
+
tins (1.3.5)
|
23
47
|
|
24
48
|
PLATFORMS
|
25
49
|
ruby
|
26
50
|
|
27
51
|
DEPENDENCIES
|
28
52
|
bundler (~> 1.7)
|
53
|
+
coveralls
|
29
54
|
rake (~> 10.0)
|
30
55
|
redlock!
|
31
|
-
rspec (~> 3.1
|
56
|
+
rspec (~> 3.1)
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# Redlock - A ruby distributed lock using redis.
|
2
2
|
|
3
|
-
Distributed locks are a very useful primitive in many environments where different processes require to operate
|
3
|
+
> Distributed locks are a very useful primitive in many environments where different processes require to operate with shared resources in a mutually exclusive way.
|
4
|
+
>
|
5
|
+
> 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.
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
This lib is an attempt to provide an implementation to a proposed distributed locks with Redis. Totally inspired by: ( http://redis.io/topics/distlock )
|
7
|
+
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)
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -22,6 +22,10 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
$ gem install redlock
|
24
24
|
|
25
|
+
## Documentation
|
26
|
+
|
27
|
+
[RubyDoc 0.0.1](http://www.rubydoc.info/gems/redlock/0.0.1/frames)
|
28
|
+
|
25
29
|
## Usage example
|
26
30
|
|
27
31
|
```ruby
|
@@ -30,7 +34,7 @@ Or install it yourself as:
|
|
30
34
|
first_try_lock_info = lock_manager.lock("resource_key", 2000)
|
31
35
|
second_try_lock_info = lock_manager.lock("resource_key", 2000)
|
32
36
|
|
33
|
-
# it prints lock info
|
37
|
+
# it prints lock info {validity: 1987, resource: "resource_key", value: "generated_uuid4"}
|
34
38
|
p first_try_lock_info
|
35
39
|
# it prints false
|
36
40
|
p second_try_lock_info
|
@@ -43,6 +47,18 @@ Or install it yourself as:
|
|
43
47
|
p second_try_lock_info
|
44
48
|
```
|
45
49
|
|
50
|
+
There's also a block version that automatically unlocks the lock:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
lock_manager.lock("resource_key", 2000) do |locked|
|
54
|
+
if locked
|
55
|
+
# critical code
|
56
|
+
else
|
57
|
+
# error handling
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
46
62
|
## Run tests
|
47
63
|
|
48
64
|
Make sure you have at least 3 redis instances `redis-server --port 777[7-9]`
|
@@ -51,7 +67,7 @@ Make sure you have at least 3 redis instances `redis-server --port 777[7-9]`
|
|
51
67
|
|
52
68
|
## Contributing
|
53
69
|
|
54
|
-
1. Fork it
|
70
|
+
1. [Fork it](https://github.com/leandromoreira/redlock-rb/fork)
|
55
71
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
56
72
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
57
73
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/lib/redlock/client.rb
CHANGED
@@ -3,6 +3,7 @@ require 'securerandom'
|
|
3
3
|
|
4
4
|
module Redlock
|
5
5
|
class Client
|
6
|
+
DEFAULT_REDIS_URLS = ['redis://localhost:6379']
|
6
7
|
DEFAULT_RETRY_COUNT = 3
|
7
8
|
DEFAULT_RETRY_DELAY = 200
|
8
9
|
CLOCK_DRIFT_FACTOR = 0.01
|
@@ -20,7 +21,7 @@ module Redlock
|
|
20
21
|
# +options+:: You can override the default value for `retry_count` and `retry_delay`.
|
21
22
|
# * `retry_count` being how many times it'll try to lock a resource (default: 3)
|
22
23
|
# * `retry_delay` being how many ms to sleep before try to lock again (default: 200)
|
23
|
-
def initialize(server_urls, options={})
|
24
|
+
def initialize(server_urls=DEFAULT_REDIS_URLS, options={})
|
24
25
|
@servers = server_urls.map {|url| Redis.new(url: url)}
|
25
26
|
@quorum = server_urls.length / 2 + 1
|
26
27
|
@retry_count = options[:retry_count] || DEFAULT_RETRY_COUNT
|
@@ -29,11 +30,35 @@ module Redlock
|
|
29
30
|
|
30
31
|
# Locks a resource for a given time. (in milliseconds)
|
31
32
|
# Params:
|
32
|
-
# +resource+:: the resource(or key) string to be locked.
|
33
|
+
# +resource+:: the resource (or key) string to be locked.
|
33
34
|
# +ttl+:: The time-to-live in ms for the lock.
|
34
|
-
def lock(resource, ttl)
|
35
|
+
def lock(resource, ttl, &block)
|
36
|
+
lock_info = lock_instances(resource, ttl)
|
37
|
+
|
38
|
+
if block_given?
|
39
|
+
begin
|
40
|
+
yield lock_info
|
41
|
+
!!lock_info
|
42
|
+
ensure
|
43
|
+
unlock(lock_info) if lock_info
|
44
|
+
end
|
45
|
+
else
|
46
|
+
lock_info
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Unlocks a resource.
|
51
|
+
# Params:
|
52
|
+
# +lock_info+:: the lock that has been acquired when you locked the resource.
|
53
|
+
def unlock(lock_info)
|
54
|
+
@servers.each{|s| unlock_instance(s, lock_info[:resource], lock_info[:value])}
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def lock_instances(resource, ttl)
|
35
60
|
value = SecureRandom.uuid
|
36
|
-
@retry_count.times
|
61
|
+
@retry_count.times do
|
37
62
|
locked_instances = 0
|
38
63
|
start_time = (Time.now.to_f * 1000).to_i
|
39
64
|
@servers.each do |s|
|
@@ -55,18 +80,11 @@ module Redlock
|
|
55
80
|
end
|
56
81
|
# Wait a random delay before to retry
|
57
82
|
sleep(rand(@retry_delay).to_f / 1000)
|
58
|
-
|
59
|
-
return false
|
60
|
-
end
|
83
|
+
end
|
61
84
|
|
62
|
-
|
63
|
-
# Params:
|
64
|
-
# +lock_info+:: the has acquired when you locked the resource.
|
65
|
-
def unlock(lock_info)
|
66
|
-
@servers.each{|s| unlock_instance(s, lock_info[:resource], lock_info[:value])}
|
85
|
+
return false
|
67
86
|
end
|
68
87
|
|
69
|
-
private
|
70
88
|
def lock_instance(redis, resource, val, ttl)
|
71
89
|
begin
|
72
90
|
return redis.client.call([:set, resource, val, :nx, :px, ttl])
|
data/lib/redlock/version.rb
CHANGED
data/redlock.gemspec
CHANGED
@@ -18,7 +18,10 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.add_dependency 'redis', '~> 3', '>= 3.0.5'
|
22
|
+
|
21
23
|
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "coveralls"
|
22
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
26
|
spec.add_development_dependency "rspec", "~> 3.1"
|
24
27
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,25 +1,97 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
2
3
|
|
3
4
|
RSpec.describe Redlock::Client do
|
4
|
-
|
5
|
-
let(:
|
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) }
|
6
8
|
let(:ttl) { 1000 }
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
10
|
+
describe 'lock' do
|
11
|
+
context 'when lock is available' do
|
12
|
+
after(:each) { lock_manager.unlock(@lock_info) if @lock_info }
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
+
it 'locks' do
|
15
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
14
16
|
|
15
|
-
|
17
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns lock information' do
|
21
|
+
@lock_info = lock_manager.lock(resource_key, ttl)
|
22
|
+
|
23
|
+
expect(@lock_info).to be_lock_info_for(resource_key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when lock is not available' do
|
28
|
+
before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
|
29
|
+
after { lock_manager.unlock(@another_lock_info) }
|
30
|
+
|
31
|
+
it 'returns false' do
|
32
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
33
|
+
|
34
|
+
expect(lock_info).to eql(false)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'block syntax' do
|
39
|
+
context 'when lock is available' do
|
40
|
+
it 'locks' do
|
41
|
+
lock_manager.lock(resource_key, ttl) do |_|
|
42
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'passes lock information as block argument' do
|
47
|
+
lock_manager.lock(resource_key, ttl) do |lock_info|
|
48
|
+
expect(lock_info).to be_lock_info_for(resource_key)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns true' do
|
53
|
+
rv = lock_manager.lock(resource_key, ttl) {}
|
54
|
+
expect(rv).to eql(true)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'automatically unlocks' do
|
58
|
+
lock_manager.lock(resource_key, ttl) {}
|
59
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'automatically unlocks when block raises exception' do
|
63
|
+
lock_manager.lock(resource_key, ttl) { fail } rescue nil
|
64
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when lock is not available' do
|
69
|
+
before { @another_lock_info = lock_manager.lock(resource_key, ttl) }
|
70
|
+
after { lock_manager.unlock(@another_lock_info) }
|
71
|
+
|
72
|
+
it 'passes false as block argument' do
|
73
|
+
lock_manager.lock(resource_key, ttl) do |lock_info|
|
74
|
+
expect(lock_info).to eql(false)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'returns false' do
|
79
|
+
rv = lock_manager.lock(resource_key, ttl) {}
|
80
|
+
expect(rv).to eql(false)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
16
84
|
end
|
17
85
|
|
18
|
-
|
19
|
-
lock_info = lock_manager.lock(resource_key, ttl)
|
20
|
-
|
21
|
-
|
86
|
+
describe 'unlock' do
|
87
|
+
before { @lock_info = lock_manager.lock(resource_key, ttl) }
|
88
|
+
|
89
|
+
it 'unlocks' do
|
90
|
+
expect(resource_key).to_not be_lockable(lock_manager, ttl)
|
91
|
+
|
92
|
+
lock_manager.unlock(@lock_info)
|
22
93
|
|
23
|
-
|
94
|
+
expect(resource_key).to be_lockable(lock_manager, ttl)
|
95
|
+
end
|
24
96
|
end
|
25
97
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,44 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
# $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
3
|
+
require 'coveralls'
|
4
|
+
Coveralls.wear!
|
3
5
|
require 'redlock'
|
6
|
+
|
7
|
+
LOCK_INFO_KEYS = %i{validity resource value}
|
8
|
+
|
9
|
+
RSpec::Matchers.define :be_lock_info_for do |resource|
|
10
|
+
def correct_type?(actual)
|
11
|
+
actual.is_a?(Hash)
|
12
|
+
end
|
13
|
+
|
14
|
+
def correct_layout?(actual)
|
15
|
+
((LOCK_INFO_KEYS | actual.keys) - (LOCK_INFO_KEYS & actual.keys)).empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def correct_resource?(actual, resource)
|
19
|
+
actual[:resource] == resource
|
20
|
+
end
|
21
|
+
|
22
|
+
match do |actual|
|
23
|
+
correct_type?(actual) && correct_layout?(actual) && correct_resource?(actual, resource)
|
24
|
+
end
|
25
|
+
|
26
|
+
failure_message do |actual|
|
27
|
+
"expected that #{actual} would be lock information for #{expected}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
RSpec::Matchers.define :be_lockable do |lock_manager, ttl|
|
32
|
+
match do |resource_key|
|
33
|
+
begin
|
34
|
+
lock_info = lock_manager.lock(resource_key, ttl)
|
35
|
+
lock_info != false
|
36
|
+
ensure
|
37
|
+
lock_manager.unlock(lock_info) if lock_info
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
failure_message do |resource_key|
|
42
|
+
"expected that #{resource_key} would be lockable"
|
43
|
+
end
|
44
|
+
end
|
metadata
CHANGED
@@ -1,55 +1,89 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redlock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.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:
|
11
|
+
date: 2015-03-19 00:00:00.000000000 Z
|
12
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
|
13
33
|
- !ruby/object:Gem::Dependency
|
14
34
|
name: bundler
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
16
36
|
requirements:
|
17
|
-
- -
|
37
|
+
- - ~>
|
18
38
|
- !ruby/object:Gem::Version
|
19
39
|
version: '1.7'
|
20
40
|
type: :development
|
21
41
|
prerelease: false
|
22
42
|
version_requirements: !ruby/object:Gem::Requirement
|
23
43
|
requirements:
|
24
|
-
- -
|
44
|
+
- - ~>
|
25
45
|
- !ruby/object:Gem::Version
|
26
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'
|
27
61
|
- !ruby/object:Gem::Dependency
|
28
62
|
name: rake
|
29
63
|
requirement: !ruby/object:Gem::Requirement
|
30
64
|
requirements:
|
31
|
-
- -
|
65
|
+
- - ~>
|
32
66
|
- !ruby/object:Gem::Version
|
33
67
|
version: '10.0'
|
34
68
|
type: :development
|
35
69
|
prerelease: false
|
36
70
|
version_requirements: !ruby/object:Gem::Requirement
|
37
71
|
requirements:
|
38
|
-
- -
|
72
|
+
- - ~>
|
39
73
|
- !ruby/object:Gem::Version
|
40
74
|
version: '10.0'
|
41
75
|
- !ruby/object:Gem::Dependency
|
42
76
|
name: rspec
|
43
77
|
requirement: !ruby/object:Gem::Requirement
|
44
78
|
requirements:
|
45
|
-
- -
|
79
|
+
- - ~>
|
46
80
|
- !ruby/object:Gem::Version
|
47
81
|
version: '3.1'
|
48
82
|
type: :development
|
49
83
|
prerelease: false
|
50
84
|
version_requirements: !ruby/object:Gem::Requirement
|
51
85
|
requirements:
|
52
|
-
- -
|
86
|
+
- - ~>
|
53
87
|
- !ruby/object:Gem::Version
|
54
88
|
version: '3.1'
|
55
89
|
description: Distributed lock using Redis written in Ruby. Highly inspired by https://github.com/antirez/redlock-rb.
|
@@ -59,8 +93,9 @@ executables: []
|
|
59
93
|
extensions: []
|
60
94
|
extra_rdoc_files: []
|
61
95
|
files:
|
62
|
-
-
|
63
|
-
-
|
96
|
+
- .gitignore
|
97
|
+
- .rspec
|
98
|
+
- .travis.yml
|
64
99
|
- Gemfile
|
65
100
|
- Gemfile.lock
|
66
101
|
- LICENSE.txt
|
@@ -82,17 +117,17 @@ require_paths:
|
|
82
117
|
- lib
|
83
118
|
required_ruby_version: !ruby/object:Gem::Requirement
|
84
119
|
requirements:
|
85
|
-
- -
|
120
|
+
- - '>='
|
86
121
|
- !ruby/object:Gem::Version
|
87
122
|
version: '0'
|
88
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
124
|
requirements:
|
90
|
-
- -
|
125
|
+
- - '>='
|
91
126
|
- !ruby/object:Gem::Version
|
92
127
|
version: '0'
|
93
128
|
requirements: []
|
94
129
|
rubyforge_project:
|
95
|
-
rubygems_version: 2.
|
130
|
+
rubygems_version: 2.0.14
|
96
131
|
signing_key:
|
97
132
|
specification_version: 4
|
98
133
|
summary: Distributed lock using Redis written in Ruby.
|