pmckee11-redis-lock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 83a23888f2a8facee8e44efa6bfed1787ccbaaff
4
+ data.tar.gz: c90fec2dd78ab7d4deb6539cb3c526771371e5b4
5
+ SHA512:
6
+ metadata.gz: 9ad344a0f17edd109ac36d756ac6fdb0bb928442223828c88666757d8ecfe4a1c142cd59f5d006f9c2fb3c7ea87ca5adf89f68f28f7e181c8df3539d968fc274
7
+ data.tar.gz: d147966d672dba6e0723759b89a40d87e16200d583a13bdb88efc7b62a2ccac049046745c805f34ce77f4011e747ee072c3bff4c5fa04dd5a7a399151541b7a2
@@ -0,0 +1,3 @@
1
+ redis-lock*.gem
2
+ *~
3
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redis-lock.gemspec
4
+ gemspec
@@ -0,0 +1,30 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pmckee11-redis-lock (0.1.0)
5
+ redis (~> 3.0, >= 3.0.5)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.2.5)
11
+ redis (3.2.0)
12
+ rspec (3.1.0)
13
+ rspec-core (~> 3.1.0)
14
+ rspec-expectations (~> 3.1.0)
15
+ rspec-mocks (~> 3.1.0)
16
+ rspec-core (3.1.7)
17
+ rspec-support (~> 3.1.0)
18
+ rspec-expectations (3.1.2)
19
+ diff-lcs (>= 1.2.0, < 2.0)
20
+ rspec-support (~> 3.1.0)
21
+ rspec-mocks (3.1.3)
22
+ rspec-support (~> 3.1.0)
23
+ rspec-support (3.1.2)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ pmckee11-redis-lock!
30
+ rspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Peter McKee
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # Redis::Lock
2
+
3
+ This gem implements robust pessimistic locking as described at http://redis.io/commands/set using the ruby redis client.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'pmckee11-redis-lock', require: 'redis-lock'
10
+
11
+ and then run bundler.
12
+
13
+ Or run
14
+
15
+ $ gem install pmckee11-redis-lock
16
+
17
+ ## Background
18
+
19
+ This implements a distributed lock with a timeout almost exactly as described in the redis documentation.
20
+ There are a few other redis lock implementations in ruby, but none of them seemed to be using the newer features in redis that can yield a performance improvement (e.g. the expanded SET parameters and Lua scripting). Using this gem requires a redis 2.6.12 server, allowing it to leverage those newer features to make fewer round trips and provide better performance
21
+
22
+ ## Contributing
23
+
24
+ 1. Fork it
25
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
26
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
27
+ 4. Push to the branch (`git push origin my-new-feature`)
28
+ 5. Create new Pull Request
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:rspec)
6
+
7
+ task :default => [:rspec]
@@ -0,0 +1,97 @@
1
+ require "redis"
2
+ require "securerandom"
3
+
4
+ class Redis
5
+ class Lock
6
+
7
+ attr_reader :redis
8
+ attr_reader :key
9
+
10
+ class AcquireLockTimeOut < StandardError
11
+ end
12
+
13
+ UNLOCK_LUA_SCRIPT = "if redis.call('get',KEYS[1])==ARGV[1] then redis.call('del',KEYS[1]) end"
14
+
15
+ # @param redis is a Redis instance
16
+ # @param key String for a unique name of the lock to acquire
17
+ # @param options[:auto_release_time] Int for the max number of seconds a lock can be held before it is auto released
18
+ # @param options[:base_sleep] Int for the number of millis to sleep after the first time a lock is not acquired
19
+ # (successive reattempts will be made with exponential back off)
20
+ def initialize(redis, key, options = {})
21
+ @redis = redis
22
+ @key = "lock:#{key}"
23
+ @auto_release_time = options[:auto_release_time] || 30
24
+ @base_sleep_in_secs = (options[:base_sleep] || 100) / 1000.0
25
+ # Unique token set as the redis value of @key when locked by this instance
26
+ @instance_name = SecureRandom.hex
27
+ # If lock was called and unlock has not yet been called, this is set to the time the lock was acquired
28
+ @time_locked = nil
29
+ end
30
+
31
+ # Acquire the lock. If a block is provided, the lock is acquired before yielding to the block and released once the
32
+ # block is returned.
33
+ # @param acquire_timeout Int for max number of seconds to spend acquiring the lock before raising an error
34
+ def lock(acquire_timeout = 10, &block)
35
+ raise AcquireLockTimeOut.new unless attempt_lock(acquire_timeout)
36
+ if block
37
+ begin
38
+ yield(self)
39
+ ensure
40
+ unlock
41
+ end
42
+ end
43
+ end
44
+
45
+ # Releases the lock if it is held by this instance. By default, this method relies on the expiration time of the key
46
+ # as a performance optimization when possible. If this is undesirable for some reason, set force_remote to true.
47
+ # @param force_remote Boolean for whether to explicitly delete on the redis server instead of relying on expiration
48
+ def unlock(force_remote = false)
49
+ # unlock is a no-op if we never called lock
50
+ if @time_locked
51
+ if Time.now < @time_locked + @auto_release_time || force_remote
52
+ @redis.eval(UNLOCK_LUA_SCRIPT, [@key], [@instance_name])
53
+ end
54
+ @time_locked = nil
55
+ end
56
+ end
57
+
58
+ # Determines whether or not the lock is held by this instance. By default, this method relies on the expiration time
59
+ # of the key as a performance optimization when possible. If this is undesirable for some reason, set force_remote
60
+ # to true.
61
+ # @param force_remote Boolean for whether to verify with a call to the redis server instead of using the lock time
62
+ # @return Boolean that is true if this lock instance currently holds the lock
63
+ def locked?(force_remote = false)
64
+ if @time_locked
65
+ if force_remote
66
+ return @redis.get(@key) == @instance_name
67
+ end
68
+ if Time.now < @time_locked + @auto_release_time
69
+ return true
70
+ end
71
+ end
72
+ return false
73
+ end
74
+
75
+ private
76
+
77
+ # @param acquire_timeout Int for the number of seconds to spend attempting to acquire the lock
78
+ # @return true if locked, false otherwise
79
+ def attempt_lock(acquire_timeout)
80
+ locked = false
81
+ sleep_time = @base_sleep_in_secs
82
+ when_to_timeout = Time.now + acquire_timeout
83
+ until locked
84
+ locked = @redis.set(@key, @instance_name, :nx => true, :ex => @auto_release_time)
85
+ unless locked
86
+ return false if Time.now > when_to_timeout
87
+ sleep(sleep_time)
88
+ # exponentially back off, but ensure that we take all of our wait time without going over
89
+ sleep_time = [sleep_time * 2, when_to_timeout - Time.now].min
90
+ end
91
+ end
92
+ @time_locked = Time.now
93
+ return true
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,5 @@
1
+ class Redis
2
+ class Lock
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'redis-lock/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pmckee11-redis-lock"
8
+ gem.version = Redis::Lock::VERSION
9
+ gem.authors = ["Peter McKee"]
10
+ gem.email = ["pmckee11@gmail.com"]
11
+ gem.description = %q{Distributed lock using ruby redis}
12
+ gem.summary = %q{Distributed lock using ruby redis}
13
+ gem.homepage = "https://github.com/pmckee11/redis-lock"
14
+ gem.license = "MIT"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "redis", '~> 3.0', '>= 3.0.5'
22
+ gem.add_development_dependency "rspec"
23
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+
3
+ describe Redis::Lock do
4
+ let(:key) {"key"}
5
+ let(:lock) {Redis::Lock.new(@redis, key)}
6
+
7
+ before(:all) do
8
+ @redis = Redis.new
9
+ end
10
+
11
+ before(:each) do
12
+ @redis.flushdb
13
+ end
14
+
15
+ after(:each) do
16
+ @redis.flushdb
17
+ end
18
+
19
+ after(:all) do
20
+ @redis.quit
21
+ end
22
+
23
+ context "#lock" do
24
+ it "should set an appropriate key in redis" do
25
+ lock.lock
26
+ @redis.get("lock:#{key}").should_not be_nil
27
+ end
28
+
29
+ context "when a block is provided" do
30
+ it "locks before yielding and releases after" do
31
+ lock.lock do |l|
32
+ l.should == lock
33
+ @redis.get("lock:#{key}").should_not be_nil
34
+ end
35
+ @redis.get("lock:#{key}").should be_nil
36
+ end
37
+ end
38
+
39
+ context "when acquire_timeout is provided" do
40
+ it "times out after the given timeout with an appropriate error" do
41
+ other_lock = Redis::Lock.new(@redis, key)
42
+ time = Time.now
43
+ lock.lock
44
+ begin
45
+ other_lock.lock(2)
46
+ fail()
47
+ rescue => e
48
+ (Time.now - time).should be_within(0.1).of(2.0)
49
+ e.should be_a(Redis::Lock::AcquireLockTimeOut)
50
+ end
51
+ end
52
+ end
53
+
54
+ context "when initialized with auto_release_time" do
55
+ it "sets the redis key with an appropriate expiration" do
56
+ other_lock = Redis::Lock.new(@redis, key, :auto_release_time => 7)
57
+ @redis.should_receive(:set).with("lock:#{key}", an_instance_of(String), :nx => true, :ex => 7).and_return(true)
58
+ other_lock.lock
59
+ end
60
+ end
61
+
62
+ context "when initialized with base_sleep" do
63
+ it "retries with exponential back off starting at base_sleep millis" do
64
+ other_lock = Redis::Lock.new(@redis, key, :base_sleep => 25)
65
+ other_lock.should_receive(:sleep).with(0.025).ordered
66
+ other_lock.should_receive(:sleep).with(0.05).ordered
67
+ other_lock.should_receive(:sleep).with(0.1).ordered
68
+ other_lock.should_receive(:sleep).with(0.2).ordered
69
+ other_lock.should_receive(:sleep) do |sleep_time|
70
+ sleep_time.should == 0.4
71
+ lock.unlock
72
+ end.ordered
73
+ lock.lock
74
+ other_lock.lock
75
+ end
76
+ end
77
+ end
78
+
79
+ context "#unlock" do
80
+ it "should delete an appropriate key from redis" do
81
+ lock.lock
82
+ lock.unlock
83
+ @redis.get("lock:#{key}").should be_nil
84
+ end
85
+
86
+ it "should not delete a lock held by another instance" do
87
+ other_lock = Redis::Lock.new(@redis, key, :auto_release_time => 1)
88
+ other_lock.lock
89
+ sleep(1.1)
90
+ lock.lock
91
+ other_lock.unlock
92
+ @redis.get("lock:#{key}").should_not be_nil
93
+ end
94
+
95
+ context "when the instance has not been locked" do
96
+ it "is a no op" do
97
+ @redis.should_not_receive(:eval)
98
+ lock.unlock
99
+ end
100
+ end
101
+
102
+ context "when the instance lock has expired based on the lock time" do
103
+ it "is a no op" do
104
+ other_lock = Redis::Lock.new(@redis, key, :auto_release_time => 1)
105
+ other_lock.lock
106
+ sleep(1)
107
+ @redis.should_not_receive(:eval)
108
+ other_lock.unlock
109
+ end
110
+
111
+ context "but force_remote is true" do
112
+ it "makes a redis call" do
113
+ other_lock = Redis::Lock.new(@redis, key, :auto_release_time => 1)
114
+ other_lock.lock
115
+ sleep(1)
116
+ @redis.should_receive(:eval).with(Redis::Lock::UNLOCK_LUA_SCRIPT, ["lock:#{key}"], instance_of(Array)).once
117
+ other_lock.unlock(true)
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ context "#locked?" do
124
+ it "correctly determines if the instance holds the lock" do
125
+ lock.locked?.should be_false
126
+ lock.lock
127
+ lock.locked?.should be_true
128
+ lock.unlock
129
+ lock.locked?.should be_false
130
+ end
131
+
132
+ context "when the instance has not been locked" do
133
+ it "is a no op" do
134
+ @redis.should_not_receive(:eval)
135
+ lock.unlock
136
+ end
137
+ end
138
+
139
+ context "when the instance lock has expired based on the lock time" do
140
+ it "is a no op" do
141
+ other_lock = Redis::Lock.new(@redis, key, :auto_release_time => 1)
142
+ other_lock.lock
143
+ sleep(1)
144
+ @redis.should_not_receive(:get)
145
+ other_lock.locked?.should be_false
146
+ end
147
+
148
+ context "but force_remote is true" do
149
+ it "makes a redis call" do
150
+ other_lock = Redis::Lock.new(@redis, key, :auto_release_time => 1)
151
+ other_lock.lock
152
+ sleep(1)
153
+ @redis.should_receive(:get).once.and_return(nil)
154
+ other_lock.locked?(true).should be_false
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ end
@@ -0,0 +1,2 @@
1
+ require 'redis'
2
+ require 'redis-lock'
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pmckee11-redis-lock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter McKee
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-26 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.0'
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.0'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.5
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: Distributed lock using ruby redis
48
+ email:
49
+ - pmckee11@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - ".gitignore"
55
+ - Gemfile
56
+ - Gemfile.lock
57
+ - LICENSE.txt
58
+ - README.md
59
+ - Rakefile
60
+ - lib/redis-lock.rb
61
+ - lib/redis-lock/version.rb
62
+ - redis-lock.gemspec
63
+ - spec/redis-lock_spec.rb
64
+ - spec/spec_helper.rb
65
+ homepage: https://github.com/pmckee11/redis-lock
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.2.2
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Distributed lock using ruby redis
89
+ test_files:
90
+ - spec/redis-lock_spec.rb
91
+ - spec/spec_helper.rb