lock_key 0.1.0

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.
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