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 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,3 @@
1
+ class RedisExpLock
2
+ VERSION = '0.1'
3
+ end
@@ -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: []