redis_exp_lock 0.1
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/LICENSE +21 -0
- data/README.md +121 -0
- data/lib/redis_exp_lock/version.rb +3 -0
- data/lib/redis_exp_lock.rb +97 -0
- data/redis_exp_lock.gemspec +22 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 80f06ec8bcc081e69fac7bc26af297a9e62180af
|
4
|
+
data.tar.gz: 8875e101298264a8983a72d1b9d076e79c48b486
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 514d2ef4aaa60255e3d3c664625c2ec06ecb669c160f7fcefefb84f6ef82443f1f60622a1fe98b7af5b509d4f54877c7337280e28242fd07b79cd193ca898531
|
7
|
+
data.tar.gz: eb34fbb2d1608563f2f9ad04983cff5c33cd7ada98f58ecd0a58e8af326b56a42a28481bb11f9c167ae35dac04741a1794e4b73a159a86504b82a615034e84f1
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Jeff Lee <jeffomatic@gmail.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# redis_exp_lock
|
2
|
+
|
3
|
+
A Ruby library providing distributed mutual exclusion using Redis. If you want to prevent multiple network nodes on from accessing a shared resource at the same time, and don't want to roll out a Zookeeper cluster (nobody's judging), this is for you. By default, locks provided by this library use an expiration time, to prevent irrecoverable locks in case the client fails.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
Redis 2.6.12 or higher, because:
|
8
|
+
|
9
|
+
- This library uses Redis's Lua functionality in order to eliminate certain race conditions on lock release. Lock expiration is handled by the Redis server itself, eliminating the need for precise time synchronization between your application hosts.
|
10
|
+
- This library uses the `SET` command with the `PX` and `NX` optional parameters. It's possible to emulate this functionality using Lua, but the current library omits this for brevity.
|
11
|
+
|
12
|
+
## Usage example
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'redis'
|
16
|
+
requie 'redis_exp_lock'
|
17
|
+
|
18
|
+
redis = Redis.new
|
19
|
+
|
20
|
+
# Create a new lock client
|
21
|
+
lock = RedisExpLock.new('lock_key', :redis => redis)
|
22
|
+
|
23
|
+
# Use the client to provide mutual exclusion.
|
24
|
+
lock.synchronize do
|
25
|
+
# Put critical section code here.
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
## Installation
|
30
|
+
|
31
|
+
Add to your Gemfile:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
gem 'redis_exp_lock'
|
35
|
+
```
|
36
|
+
|
37
|
+
## API
|
38
|
+
|
39
|
+
### ::initialize(lock_key, opts)
|
40
|
+
|
41
|
+
Creates a new lock client. The constructor takes two parameters, a lock key and and a hash of configuration options.
|
42
|
+
|
43
|
+
The **lock key** key identifies a resource to be locked. Clients with the same lock key can only acquire the lock one at a time.
|
44
|
+
|
45
|
+
The **options hash** takes the following options:
|
46
|
+
|
47
|
+
##### :redis
|
48
|
+
|
49
|
+
(required) An instance of a Redis client that obeys the [redis-rb](https://github.com/redis/redis-rb) interface.
|
50
|
+
|
51
|
+
##### :expiry
|
52
|
+
|
53
|
+
The lifetime of the lock, in seconds. A lock's lifetime begins counting down as soon as the Redis key corresponding to the lock is successfully set. Set this to `nil` if you want a non-expiring lock, which is not recommended. Defaults to 60 seconds.
|
54
|
+
|
55
|
+
##### :retries
|
56
|
+
|
57
|
+
In the event that the lock has been acquired by another client, the current client can automatically attempt to re-acquire the lock, up to a configurable number of attempts. Defaults to zero (no automatic retries).
|
58
|
+
|
59
|
+
##### :interval
|
60
|
+
|
61
|
+
The amount of time in seconds that the client will wait before attempting to re-acquire the lock. Defaults to 0.01 (ten milliseconds).
|
62
|
+
|
63
|
+
### #locked?
|
64
|
+
|
65
|
+
Without calling into Redis, returns whether the local lock is in a lock state, i.e., `lock` has been called without a corresponding `unlock`.
|
66
|
+
|
67
|
+
### #key_locked?
|
68
|
+
|
69
|
+
Calls remotely to Redis to determine if the lock key has been locked by *any* client.
|
70
|
+
|
71
|
+
### #key_owned?
|
72
|
+
|
73
|
+
Calls remotely to Redis to determine if the client currently owns the lock.
|
74
|
+
|
75
|
+
### #try_lock
|
76
|
+
|
77
|
+
Attempts to obtain the lock and returns immediately. Returns `true` if the lock was successfully acquired, otherwise returns `false`.
|
78
|
+
|
79
|
+
- Raises `RedisExpLock::AlreadyAcquiredLockError` if client's lock state has not yet been cleared, i.e., if `#lock` was previously called without a corresponding `#unlock`.
|
80
|
+
|
81
|
+
### #lock
|
82
|
+
|
83
|
+
Attempts to grab the lock, and waits if it isn’t available. Returns the number of attempts required to acquire the lock.
|
84
|
+
|
85
|
+
- Raises `RedisExpLock::AlreadyAcquiredLockError` if client's lock state has not yet been cleared, i.e., if `#lock` was previously called without a corresponding `#unlock`.
|
86
|
+
- Raises `RedisExpLock::TooManyLockAttemptsError` if the max number of retries is exceed when attempting to acquire the lock.
|
87
|
+
|
88
|
+
### #unlock
|
89
|
+
|
90
|
+
Releases the lock. Returns `true` if the lock was successfully released. Returns `false` if the client never acquired the lock, or the lock expired before the unlock.
|
91
|
+
|
92
|
+
### #synchronize(&block)
|
93
|
+
|
94
|
+
Obtains a lock, runs the supplied block, and releases the lock when the block completes. Yields the number of attempts required to acquire the lock.
|
95
|
+
|
96
|
+
- Raises `RedisExpLock::AlreadyAcquiredLockError` if client's lock state has not yet been cleared, i.e., if `#lock` was previously called without a corresponding `#unlock`.
|
97
|
+
- Raises `RedisExpLock::TooManyLockAttemptsError` if the max number of retries is exceed when attempting to acquire the lock.
|
98
|
+
|
99
|
+
## Algorithm
|
100
|
+
|
101
|
+
### Lock acquisition
|
102
|
+
|
103
|
+
Locks are acquired by setting a Redis key with a UUID generated immediately prior to the lock attempt.
|
104
|
+
|
105
|
+
Since the Redis server manages the lifetime, there is no need for any client-side logic that deals with lock expiration, and thus no need to ensure that clients are time-synchronized.
|
106
|
+
|
107
|
+
### Lock release
|
108
|
+
|
109
|
+
Locks are released by deleting the Redis key, **only if** the key's value matches the UUID generated during a successful lock attempt. A Lua script provides the following atomic sequence:
|
110
|
+
|
111
|
+
1. `GET` the value of the key.
|
112
|
+
2. If the value of the key is the same as the UUID, then use `DEL` to remove the key.
|
113
|
+
|
114
|
+
Without using a Lua script to ensure atomicity, it's possible to encounter subtle race conditions, in which another client acquires the lock between the two steps above.
|
115
|
+
|
116
|
+
### Caveats
|
117
|
+
|
118
|
+
Lock clients in this library rely on prompt responses from the Redis server. Under aberrant network conditions, there are some edge cases that may cause your application to get out of whack. For example:
|
119
|
+
|
120
|
+
1. Lock acquisition requests may be received by the Redis server, but the response could fail to reach the client (e.g., in a socket timeout on the client side). In other words, it's possible that the Redis server thinks the lock has been acquired, but the local client doesn't. If this is a serious concern, be sure to set an expiry value so that the lock can automatically be released by the Redis server.
|
121
|
+
2. For expiring locks: it may take a while for the Redis server to respond to a lock acquisition request. This may result in situations where your critical section may not have the full lifetime of the lock to complete. In extreme cases, it's possible for the lock to have already expired on the server by the time the client recognizes its acquisition. If this is a serious concern, consider wrapping the lock acquisition with a timer to make sure you actually have enough time to proceed with your critical section.
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'shavaluator'
|
2
|
+
|
3
|
+
class RedisExpLock
|
4
|
+
|
5
|
+
# Errors
|
6
|
+
class TooManyLockAttemptsError < RuntimeError; end
|
7
|
+
class AlreadyAcquiredLockError < RuntimeError; end
|
8
|
+
|
9
|
+
LUA = {
|
10
|
+
# Deletes keys if they equal the given values
|
11
|
+
:delequal => """
|
12
|
+
local deleted = 0
|
13
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
14
|
+
return redis.call('DEL', KEYS[1])
|
15
|
+
end
|
16
|
+
return 0
|
17
|
+
""",
|
18
|
+
}
|
19
|
+
|
20
|
+
attr_reader :redis, :lock_key, :lock_uuid, :expiry, :retries, :retry_interval
|
21
|
+
|
22
|
+
def initialize(lock_key, opts)
|
23
|
+
defaults = {
|
24
|
+
:expiry => nil, # in seconds
|
25
|
+
:retries => 0,
|
26
|
+
:retry_interval => 0.01, # in seconds
|
27
|
+
}
|
28
|
+
opts = {}.merge(defaults).merge(opts)
|
29
|
+
|
30
|
+
@lock_key = lock_key.to_s
|
31
|
+
raise ArgumentError.new('Invalid lock key') unless @lock_key.size > 0
|
32
|
+
@lock_uuid = nil
|
33
|
+
|
34
|
+
@redis = opts[:redis]
|
35
|
+
@shavaluator = Shavaluator.new(:redis => @redis)
|
36
|
+
@shavaluator.add(LUA)
|
37
|
+
|
38
|
+
@expiry = opts[:expiry]
|
39
|
+
@retries = opts[:retries]
|
40
|
+
@retry_interval = opts[:retry_interval]
|
41
|
+
end
|
42
|
+
|
43
|
+
def locked?
|
44
|
+
!@lock_uuid.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def key_locked?
|
48
|
+
@redis.exists(@lock_key)
|
49
|
+
end
|
50
|
+
|
51
|
+
def key_owned?
|
52
|
+
!@lock_uuid.nil? && @lock_uuid == @redis.get(@lock_key)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Attempt to acquire the lock, returning true if the lock was succesfully
|
56
|
+
# acquired, and false if not.
|
57
|
+
def try_lock
|
58
|
+
raise AlreadyAcquiredLockError if locked?
|
59
|
+
|
60
|
+
uuid = SecureRandom.uuid
|
61
|
+
set_opts = {
|
62
|
+
:nx => true
|
63
|
+
}
|
64
|
+
set_opts[:px] = Integer(@expiry * 1000) if @expiry
|
65
|
+
|
66
|
+
if @redis.set(@lock_key, uuid, set_opts)
|
67
|
+
@lock_uuid = uuid
|
68
|
+
true
|
69
|
+
else
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def lock
|
75
|
+
attempts = 0
|
76
|
+
while attempts <= @retries
|
77
|
+
attempts += 1
|
78
|
+
return attempts if try_lock
|
79
|
+
sleep @retry_interval
|
80
|
+
end
|
81
|
+
raise TooManyLockAttemptsError
|
82
|
+
end
|
83
|
+
|
84
|
+
def unlock
|
85
|
+
return false unless locked?
|
86
|
+
was_locked = @shavaluator.exec(:delequal, :keys => [@lock_key], :argv => [@lock_uuid]) == 1
|
87
|
+
@lock_uuid = nil
|
88
|
+
was_locked
|
89
|
+
end
|
90
|
+
|
91
|
+
def synchronize(&crit_sec)
|
92
|
+
attempts = lock
|
93
|
+
crit_sec.call attempts
|
94
|
+
unlock
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.unshift File.expand_path("../lib", __FILE__)
|
3
|
+
require 'redis_exp_lock/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'redis_exp_lock'
|
7
|
+
s.licenses = ['MIT']
|
8
|
+
s.summary = "Distributed mutual exclusion using Redis"
|
9
|
+
s.version = RedisExpLock::VERSION
|
10
|
+
s.homepage = 'https://github.com/jeffomatic/redis_exp_lock_rb'
|
11
|
+
|
12
|
+
s.authors = ["Jeff Lee"]
|
13
|
+
s.email = 'jeffomatic@gmail.com'
|
14
|
+
|
15
|
+
s.files = %w( README.md LICENSE redis_exp_lock.gemspec )
|
16
|
+
s.files += Dir.glob('lib/**/*')
|
17
|
+
|
18
|
+
s.add_runtime_dependency('shavaluator', '~>0.1')
|
19
|
+
|
20
|
+
s.add_development_dependency('rspec', '~>3.1.0')
|
21
|
+
s.add_development_dependency('redis', '~>3.2.0')
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redis_exp_lock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeff Lee
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: shavaluator
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.1.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: redis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.2.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.2.0
|
55
|
+
description:
|
56
|
+
email: jeffomatic@gmail.com
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
- lib/redis_exp_lock.rb
|
64
|
+
- lib/redis_exp_lock/version.rb
|
65
|
+
- redis_exp_lock.gemspec
|
66
|
+
homepage: https://github.com/jeffomatic/redis_exp_lock_rb
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 2.4.2
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Distributed mutual exclusion using Redis
|
90
|
+
test_files: []
|