lock_key 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in lock_key.gemspec
4
+ gemspec
5
+ gem 'rake'
6
+ gem 'rspec'
7
+ gem 'redis'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 TODO: Write your name
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.
data/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # LockKey
2
+
3
+ Provides basic locking in Redis
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'lock_key'
10
+
11
+ If running on 1.9 that's it, if not, you'll need to also install uuid
12
+
13
+ gem 'uuid'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install lock_key
22
+
23
+ ## Usage
24
+
25
+ Based on [redis-lock](https://github.com/PatrickTulskie/redis-lock) and the [setnx redis comments](http://redis.io/commands/setnx)
26
+ LockKey provides basic key leve locking.
27
+
28
+ ## Locking a key
29
+
30
+ r = Redis.new
31
+
32
+ # use the Redis::LockKey.defaults
33
+ r.lock_key "some_key" do
34
+ # stuff in here with the key locked
35
+ end
36
+
37
+ # Selectively overwrite the defaults
38
+ r.lock_key "some_key", :expire => 3 do
39
+ # stuff in here with the key locked
40
+ end
41
+
42
+ Using the block version of lock\_key ensures that the lock is removed at the end of the block
43
+
44
+ If you need more control over the locking and unlocking do not use a block. Just be sure to ensure you remove the lock.
45
+
46
+ r.lock_key "some_key"
47
+ # do stuff here
48
+ r.unlock_key "some_key"
49
+
50
+ If worst comes to worst, you can forcefully kill the lock
51
+
52
+ r.kill_lock! "some_key"
53
+
54
+ NOTE: You should always minimise the size of the lock. Do your best not to wrap external calls in a lock
55
+
56
+ ## Contributing
57
+
58
+ 1. Fork it
59
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
60
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
61
+ 4. Push to the branch (`git push origin my-new-feature`)
62
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ # Rakefile
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
@@ -0,0 +1,156 @@
1
+ class Redis
2
+ module LockKey
3
+ begin
4
+ require 'securerandom'
5
+ UUID_GEN = lambda { SecureRandom.uuid }
6
+ rescue LoadError
7
+ begin
8
+ require 'uuid'
9
+ UUID_GEN = lambda { UUID.new.generate }
10
+ rescue LoadError
11
+ puts <<-TXT
12
+ Could not find a uuid generator.
13
+ Ensure ActiveSupport is available for SecureRandom
14
+ OR
15
+ Install uuid gem
16
+ We prefer SecureRandom
17
+ TXT
18
+ end
19
+ end
20
+
21
+ class LockAttemptTimeout < StandardError; end
22
+
23
+ @@defaults = {
24
+ :wait_for => 60, # seconds to wait to obtain a lock
25
+ :expire => 60, # seconds till key expires
26
+ :raise => true, # raise a LockKey::LockAttemptTimeout if the lock cannot be obtained
27
+ :sleep_for => 0.5
28
+ }
29
+
30
+ @@value_delimeter = "-:-:-"
31
+
32
+ def self.value_delimeter; @@value_delimeter; end
33
+ def self.value_delimeter=(del); @@value_delimeter = del; end
34
+
35
+ def self.defaults=(defaults); @@defaults = @@defaults.merge(defaults); end
36
+ def self.defaults; @@defaults; end
37
+
38
+ # The lock key id for this thread. Uses uuid so that concurrency is not an issue
39
+ # w.r.t. keys
40
+ def self.lock_key_id; Thread.current[:lock_key_id] ||= UUID_GEN.call; end
41
+
42
+ # Locks a key in redis options are same as default.
43
+ # If a block is given the lock is automatically released
44
+ # If no block is given, be sure to unlock the key when you're done.
45
+ # Note... Locks should be as _Small_ as possible with respec to the time you
46
+ # have the lock for!
47
+ # @param key String The key to lock
48
+ # @param opts Hash the options hash for the lock
49
+ # @option opts :wait_for Numeric The time to wait for to obtain a lock
50
+ # @option opts :expire Numeric The time before the lock expires
51
+ # @option opts :raise Causes a raise if a lock cannot be obtained
52
+ # @option opts :sleep_for the time to sleep between checks
53
+ def lock_key(key, opts={})
54
+ is_block, got_lock = block_given?, false
55
+ options = LockKey.defaults.merge(opts)
56
+
57
+ got_lock = obtain_lock(key, options)
58
+ yield if is_block && got_lock
59
+ got_lock
60
+ ensure
61
+ unlock_key(key, options) if is_block && got_lock
62
+ end
63
+
64
+ def locked_key?(key)
65
+ !lock_expired?(_redis_.get(lock_key_for(key)))
66
+ end
67
+
68
+ def kill_lock!(key)
69
+ _redis_.del(lock_key_for(key))
70
+ end
71
+
72
+ # Unlocks the key. Use a block... then you don't need this
73
+ # @param key String the key to unlock
74
+ # @param opts Hash an options hash
75
+ # @option opts :key the value of the key to unlock.
76
+ #
77
+ # @example
78
+ # # Unlock the key if this thread owns it.
79
+ # redis.lock_key "foo"
80
+ # # do stuff
81
+ # redis.unlock_key "foo"
82
+ #
83
+ # @example
84
+ # # Unlock the key in a multithreaded env
85
+ # key_value = redis.lock_key "foo"
86
+ # Thread.new do
87
+ # # do stuff
88
+ # redis.unlock_key "foo", :key => key_value
89
+ # end
90
+ def unlock_key(key, opts={})
91
+ lock_key = opts[:key]
92
+ value = _redis_.get(lock_key_for(key))
93
+ return true unless value
94
+ if value == lock_key || i_have_the_lock?(value)
95
+ kill_lock!(key)
96
+ true
97
+ else
98
+ false
99
+ end
100
+ end
101
+
102
+ private
103
+ def _redis_
104
+ self
105
+ end
106
+
107
+ def lock_key_for(key)
108
+ "lock_key:#{key}"
109
+ end
110
+
111
+ def lock_value_for(key, opts)
112
+ "#{(Time.now + opts[:expire]).to_i}#{value_delimeter}#{LockKey.lock_key_id}"
113
+ end
114
+
115
+ def value_delimeter
116
+ LockKey.value_delimeter
117
+ end
118
+
119
+ def obtain_lock(key, opts={})
120
+ _key_ = lock_key_for(key)
121
+ _value_ = lock_value_for(key,opts)
122
+ return _value_ if _redis_.setnx(_key_, _value_)
123
+
124
+ got_lock = false
125
+ wait_until = Time.now + opts[:wait_for]
126
+
127
+ until got_lock || Time.now > wait_until
128
+ current_lock = _redis_.get(_key_)
129
+ if lock_expired?(current_lock)
130
+ _value_ = lock_value_for(key,opts)
131
+ new_lock = _redis_.getset(_key_, _value_)
132
+ got_lock = new_lock if i_have_the_lock?(new_lock)
133
+ end
134
+ sleep opts[:sleep_for]
135
+ end
136
+
137
+ if !got_lock && opts[:raise]
138
+ _value_ = lock_value_for(key, opts)
139
+ raise LockAttemptTimeout, "Could not lock #{_value_}"
140
+ end
141
+
142
+ got_lock
143
+ end
144
+
145
+ def lock_expired?(lock_value)
146
+ return true if lock_value.nil?
147
+ exp = lock_value.split(value_delimeter).first
148
+ Time.now.to_i > exp.to_i
149
+ end
150
+
151
+ def i_have_the_lock?(lock_value)
152
+ return false unless lock_value
153
+ lock_value.split(value_delimeter).last == LockKey.lock_key_id
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,3 @@
1
+ module LockKey
2
+ VERSION = "0.1.0"
3
+ end
data/lib/lock_key.rb ADDED
@@ -0,0 +1,7 @@
1
+ require "lock_key/version"
2
+ require 'lock_key/lock_key'
3
+ require 'redis'
4
+
5
+ class Redis
6
+ include Redis::LockKey
7
+ end
data/lock_key.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'lock_key/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "lock_key"
8
+ gem.version = LockKey::VERSION
9
+ gem.authors = ["Take out locks via redis"]
10
+ gem.email = ["has.sox@gmail.com"]
11
+ gem.description = %q{Uses redis to take out multi-threaded/processed safe locks}
12
+ gem.summary = %q{Uses redis to take out multi-threaded/processed safe locks}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe "LockKey" do
4
+ before do
5
+ REDIS.flushdb
6
+ end
7
+
8
+ after do
9
+ REDIS.unlock_key "foo"
10
+ end
11
+
12
+ it "takes out a lock" do
13
+ REDIS.lock_key "foo"
14
+ REDIS.locked_key?("foo").should be_true
15
+ end
16
+
17
+ it "removes a lock" do
18
+ REDIS.lock_key "foo" do
19
+ REDIS.locked_key?("foo").should be_true
20
+ end
21
+ REDIS.locked_key?("foo").should be_false
22
+ end
23
+
24
+ it "removes a lock manually" do
25
+ REDIS.lock_key "foo"
26
+ REDIS.locked_key?("foo").should be_true
27
+ REDIS.unlock_key "foo"
28
+ REDIS.locked_key?("foo").should be_false
29
+ end
30
+
31
+ it "handles many threads" do
32
+ captures = []
33
+ one = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 2; captures << :one } }
34
+ two = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 1; captures << :two } }
35
+ three = lambda{ REDIS.lock_key("foo", :expire => 5) { sleep 2; captures << :three } }
36
+ four = lambda{ REDIS.lock_key("foo", :expire => 1, wait_for: 1) { sleep 2; captures << :four } }
37
+
38
+ threads = []
39
+
40
+ threads << Thread.new(&one)
41
+ threads << Thread.new(&two)
42
+ threads << Thread.new(&three)
43
+ threads << Thread.new(&four)
44
+
45
+ threads.each { |t| t.join }
46
+
47
+ captures.should have(3).elements
48
+ captures.should_not include(:four)
49
+ captures.should include(:one, :two, :three)
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ require 'lock_key'
2
+
3
+ REDIS = Redis.new
4
+ puts REDIS.inspect
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lock_key
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Take out locks via redis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-06 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Uses redis to take out multi-threaded/processed safe locks
15
+ email:
16
+ - has.sox@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/lock_key.rb
27
+ - lib/lock_key/lock_key.rb
28
+ - lib/lock_key/version.rb
29
+ - lock_key.gemspec
30
+ - spec/lock_key_spec.rb
31
+ - spec/spec_helper.rb
32
+ homepage: ''
33
+ licenses: []
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubyforge_project:
52
+ rubygems_version: 1.8.24
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: Uses redis to take out multi-threaded/processed safe locks
56
+ test_files:
57
+ - spec/lock_key_spec.rb
58
+ - spec/spec_helper.rb