locker 0.1.0 → 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/README.md +9 -0
- data/lib/locker.rb +1 -0
- data/lib/locker/advisory.rb +117 -0
- data/lib/locker/version.rb +1 -1
- data/spec/locker/advisory_spec.rb +75 -0
- metadata +17 -45
- data/autotest/discover.rb +0 -1
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
data.tar.gz: 5de726ed22ab717d6b7d4c46b78f05ce2d1ffe80
|
4
|
+
metadata.gz: 1c082e3e46be1848f8ebaad6a48f9fd640fda707
|
5
|
+
SHA512:
|
6
|
+
data.tar.gz: 1500e4acd3c15fb9d864bbdbdb30b1fd6d92808d63fe1acd904046ed17f089553706939a54adb7723334eca23e1c298eb7ac66708308de56c0ef82fc5dc372de
|
7
|
+
metadata.gz: 91f9f3529ffe477582aed59b861a446134b055e4e293d40daf39596f6ae6c1e2fa5d03209a7145c00126f721f9deb11704bdf7a5ffbc4f4fbe341dacfc62d18f
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.2.0
|
4
|
+
|
5
|
+
[Full Changelog](http://github.com/zencoder/locker/compare/v0.1.0...v0.2.0)
|
6
|
+
|
7
|
+
Enhancements:
|
8
|
+
|
9
|
+
* Added the ability to use Postgres advisory locks via Locker::Advisory.
|
10
|
+
|
11
|
+
## 0.1.0
|
12
|
+
|
13
|
+
[Full Changelog](http://github.com/zencoder/locker/compare/v0.0.3...v0.1.0)
|
14
|
+
|
15
|
+
**NOTE:** You will need to manually add the `sequence` column. See the README for more information.
|
16
|
+
|
17
|
+
Enhancements:
|
18
|
+
|
19
|
+
* Added a `sequence` column that gets incremented when a lock is acquired. The value of sequence is then passed into the lock block.
|
20
|
+
* Added multi-ruby testing using Travis CI. Tests passes in just about everything.
|
21
|
+
* Now supports MRI 1.8.7+, rbx, and jruby.
|
22
|
+
|
23
|
+
Bugfixes:
|
24
|
+
|
25
|
+
* Removed weirdness around defaulting to using ActiveSupport::SecureRandom when SecureRandom was missing, which should have never been the case.
|
data/README.md
CHANGED
@@ -14,6 +14,15 @@ In Rails 3.x+:
|
|
14
14
|
|
15
15
|
script/rails generate migration add_sequence_to_locks sequence:bigint
|
16
16
|
|
17
|
+
Then add a line that changes the default of the column to zero and updates the existing records:
|
18
|
+
|
19
|
+
change_column_default :locks, :sequence, 0
|
20
|
+
execute "UPDATE locks SET sequence = 0"
|
21
|
+
|
22
|
+
## Supported Rubies
|
23
|
+
|
24
|
+
See [the travis configuration file](https://github.com/zencoder/locker/blob/master/.travis.yml) for which ruby versions we support.
|
25
|
+
|
17
26
|
## The Basics
|
18
27
|
|
19
28
|
In its simplest form it can be used as follows:
|
data/lib/locker.rb
CHANGED
@@ -0,0 +1,117 @@
|
|
1
|
+
require "zlib"
|
2
|
+
|
3
|
+
class Locker
|
4
|
+
class Advisory
|
5
|
+
class LockConnectionLost < StandardError; end
|
6
|
+
|
7
|
+
attr_reader :key, :crc, :lockspace, :blocking, :locked
|
8
|
+
|
9
|
+
MAX_LOCK = 2147483647
|
10
|
+
MIN_LOCK = -2147483648
|
11
|
+
OVERFLOW_ADJUSTMENT = 2**32
|
12
|
+
|
13
|
+
def initialize(key, options={})
|
14
|
+
raise ArgumentError, "key must be a string" unless key.is_a?(String)
|
15
|
+
|
16
|
+
@key = key
|
17
|
+
@crc = convert_to_crc(key)
|
18
|
+
@lockspace = (options[:lockspace] || 1)
|
19
|
+
@blocking = !!options[:blocking]
|
20
|
+
@locked = false
|
21
|
+
|
22
|
+
if !@lockspace.is_a?(Integer) || @lockspace < MIN_LOCK || @lockspace > MAX_LOCK
|
23
|
+
raise ArgumentError, "The :lockspace option must be an integer between #{MIN_LOCK} and #{MAX_LOCK}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.run(key, options={}, &block)
|
28
|
+
advisory = new(key, options)
|
29
|
+
advisory.run(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def run(&block)
|
33
|
+
connection = ActiveRecord::Base.connection_pool.checkout
|
34
|
+
connection.transaction :requires_new => true do
|
35
|
+
ensure_unlocked(connection)
|
36
|
+
|
37
|
+
while !get(connection) && @blocking
|
38
|
+
sleep 0.5
|
39
|
+
end
|
40
|
+
|
41
|
+
if @locked
|
42
|
+
begin
|
43
|
+
parent_thread = Thread.current
|
44
|
+
|
45
|
+
mutex = Mutex.new
|
46
|
+
|
47
|
+
checker = Thread.new do
|
48
|
+
while @locked
|
49
|
+
10.times{ sleep 0.5 if @locked }
|
50
|
+
mutex.synchronize do
|
51
|
+
if @locked
|
52
|
+
check(connection, parent_thread)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
block.call
|
59
|
+
ensure
|
60
|
+
release(connection) if @locked
|
61
|
+
# Using a mutex to synchronize so that we're sure we're not
|
62
|
+
# executing a query when we kill the thread.
|
63
|
+
mutex.synchronize{}
|
64
|
+
if checker.alive?
|
65
|
+
checker.exit rescue nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
true
|
69
|
+
else
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
ensure
|
74
|
+
ActiveRecord::Base.connection_pool.checkin(connection) if connection
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def get(connection)
|
80
|
+
result = exec_query(connection, "SELECT pg_try_advisory_xact_lock(#{connection.quote(@lockspace)}, #{connection.quote(@crc)})")
|
81
|
+
@locked = successful_result?(result)
|
82
|
+
end
|
83
|
+
|
84
|
+
def release(connection)
|
85
|
+
result = exec_query(connection, "SELECT pg_advisory_unlock(#{connection.quote(@lockspace)}, #{connection.quote(@crc)})")
|
86
|
+
successful_result?(result)
|
87
|
+
end
|
88
|
+
|
89
|
+
def ensure_unlocked(connection)
|
90
|
+
while release(connection); end
|
91
|
+
end
|
92
|
+
|
93
|
+
def check(connection, thread)
|
94
|
+
if !connection.active?
|
95
|
+
@locked = false
|
96
|
+
thread.raise LockConnectionLost
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def convert_to_crc(key)
|
101
|
+
crc = Zlib.crc32(key)
|
102
|
+
crc -= OVERFLOW_ADJUSTMENT if crc > MAX_LOCK
|
103
|
+
crc
|
104
|
+
end
|
105
|
+
|
106
|
+
def successful_result?(result)
|
107
|
+
result.rows.size == 1 && result.rows[0].size == 1 && result.rows[0][0] == "t"
|
108
|
+
end
|
109
|
+
|
110
|
+
def exec_query(connection, query)
|
111
|
+
silence_stderr do
|
112
|
+
connection.exec_query(query, "Locker::Advisory")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
data/lib/locker/version.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Locker::Advisory do
|
4
|
+
|
5
|
+
describe "initialization" do
|
6
|
+
it "should have default values" do
|
7
|
+
advisory = Locker::Advisory.new("foo")
|
8
|
+
advisory.key.should == "foo"
|
9
|
+
advisory.crc.should == -1938594527
|
10
|
+
advisory.lockspace.should == 1
|
11
|
+
advisory.blocking.should be_false
|
12
|
+
advisory.locked.should be_false
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should have some overridable values" do
|
16
|
+
advisory = Locker::Advisory.new("foo", :lockspace => 2, :blocking => true)
|
17
|
+
advisory.lockspace.should == 2
|
18
|
+
advisory.blocking.should be_true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should validate key" do
|
22
|
+
expect{ Locker::Advisory.new(1) }.to raise_error(ArgumentError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should validate lockspace" do
|
26
|
+
expect{ Locker::Advisory.new("foo", :lockspace => Locker::Advisory::MIN_LOCK - 1) }.to raise_error(ArgumentError)
|
27
|
+
expect{ Locker::Advisory.new("foo", :lockspace => Locker::Advisory::MAX_LOCK + 1) }.to raise_error(ArgumentError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "locking" do
|
32
|
+
it "should lock to the exclusion of other locks and return true on success and false on failure" do
|
33
|
+
lock1 = false
|
34
|
+
lock2 = false
|
35
|
+
|
36
|
+
t = Thread.new do
|
37
|
+
Locker::Advisory.run("foo") do
|
38
|
+
lock1 = true
|
39
|
+
sleep 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Thread.pass
|
44
|
+
|
45
|
+
lock2_result = Locker::Advisory.run("foo") do
|
46
|
+
lock2 = true
|
47
|
+
end
|
48
|
+
|
49
|
+
lock1_result = t.join
|
50
|
+
lock1.should be_true
|
51
|
+
lock2.should be_false
|
52
|
+
lock1_result.should be_true
|
53
|
+
lock2_result.should be_false
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should release locks after the block is finished" do
|
57
|
+
lock1 = false
|
58
|
+
lock2 = false
|
59
|
+
|
60
|
+
lock1_result = Locker::Advisory.run("foo") do
|
61
|
+
lock1 = true
|
62
|
+
end
|
63
|
+
|
64
|
+
lock2_result = Locker::Advisory.run("foo") do
|
65
|
+
lock2 = true
|
66
|
+
end
|
67
|
+
|
68
|
+
lock1.should be_true
|
69
|
+
lock2.should be_true
|
70
|
+
lock1_result.should be_true
|
71
|
+
lock2_result.should be_true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: locker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 1
|
9
|
-
- 0
|
10
|
-
version: 0.1.0
|
4
|
+
version: 0.2.0
|
11
5
|
platform: ruby
|
12
6
|
authors:
|
13
7
|
- Nathan Sutton
|
@@ -16,21 +10,15 @@ autorequire:
|
|
16
10
|
bindir: bin
|
17
11
|
cert_chain: []
|
18
12
|
|
19
|
-
date:
|
13
|
+
date: 2014-05-10 00:00:00 Z
|
20
14
|
dependencies:
|
21
15
|
- !ruby/object:Gem::Dependency
|
22
16
|
name: activerecord
|
23
17
|
prerelease: false
|
24
18
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
19
|
requirements:
|
27
20
|
- - ">="
|
28
21
|
- !ruby/object:Gem::Version
|
29
|
-
hash: 31
|
30
|
-
segments:
|
31
|
-
- 2
|
32
|
-
- 3
|
33
|
-
- 14
|
34
22
|
version: 2.3.14
|
35
23
|
type: :runtime
|
36
24
|
version_requirements: *id001
|
@@ -38,30 +26,21 @@ dependencies:
|
|
38
26
|
name: pg
|
39
27
|
prerelease: false
|
40
28
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
29
|
requirements:
|
43
|
-
-
|
30
|
+
- &id003
|
31
|
+
- ">="
|
44
32
|
- !ruby/object:Gem::Version
|
45
|
-
hash: 3
|
46
|
-
segments:
|
47
|
-
- 0
|
48
33
|
version: "0"
|
49
34
|
type: :development
|
50
35
|
version_requirements: *id002
|
51
36
|
- !ruby/object:Gem::Dependency
|
52
37
|
name: rspec
|
53
38
|
prerelease: false
|
54
|
-
requirement: &
|
55
|
-
none: false
|
39
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
56
40
|
requirements:
|
57
|
-
-
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
hash: 3
|
60
|
-
segments:
|
61
|
-
- 0
|
62
|
-
version: "0"
|
41
|
+
- *id003
|
63
42
|
type: :development
|
64
|
-
version_requirements: *
|
43
|
+
version_requirements: *id004
|
65
44
|
description: Locker is a locking mechanism for limiting the concurrency of ruby code using the database. It presently only works with PostgreSQL.
|
66
45
|
email:
|
67
46
|
- nate@zencoder.com
|
@@ -75,11 +54,11 @@ extra_rdoc_files: []
|
|
75
54
|
files:
|
76
55
|
- .gitignore
|
77
56
|
- .travis.yml
|
57
|
+
- CHANGELOG.md
|
78
58
|
- Gemfile
|
79
59
|
- LICENSE
|
80
60
|
- README.md
|
81
61
|
- Rakefile
|
82
|
-
- autotest/discover.rb
|
83
62
|
- gemfiles/rails2.gemfile
|
84
63
|
- gemfiles/rails3.gemfile
|
85
64
|
- generators/locker/USAGE
|
@@ -91,45 +70,38 @@ files:
|
|
91
70
|
- lib/generators/locker/templates/migration.rb
|
92
71
|
- lib/generators/locker/templates/model.rb
|
93
72
|
- lib/locker.rb
|
73
|
+
- lib/locker/advisory.rb
|
94
74
|
- lib/locker/locker.rb
|
95
75
|
- lib/locker/version.rb
|
96
76
|
- locker.gemspec
|
77
|
+
- spec/locker/advisory_spec.rb
|
97
78
|
- spec/locker/locker_spec.rb
|
98
79
|
- spec/spec_helper.rb
|
99
80
|
homepage:
|
100
81
|
licenses: []
|
101
82
|
|
83
|
+
metadata: {}
|
84
|
+
|
102
85
|
post_install_message:
|
103
86
|
rdoc_options: []
|
104
87
|
|
105
88
|
require_paths:
|
106
89
|
- lib
|
107
90
|
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
-
none: false
|
109
91
|
requirements:
|
110
|
-
-
|
111
|
-
- !ruby/object:Gem::Version
|
112
|
-
hash: 3
|
113
|
-
segments:
|
114
|
-
- 0
|
115
|
-
version: "0"
|
92
|
+
- *id003
|
116
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
-
none: false
|
118
94
|
requirements:
|
119
|
-
-
|
120
|
-
- !ruby/object:Gem::Version
|
121
|
-
hash: 3
|
122
|
-
segments:
|
123
|
-
- 0
|
124
|
-
version: "0"
|
95
|
+
- *id003
|
125
96
|
requirements: []
|
126
97
|
|
127
98
|
rubyforge_project: locker
|
128
|
-
rubygems_version:
|
99
|
+
rubygems_version: 2.0.14
|
129
100
|
signing_key:
|
130
|
-
specification_version:
|
101
|
+
specification_version: 4
|
131
102
|
summary: Locker is a locking mechanism for limiting the concurrency of ruby code using the database.
|
132
103
|
test_files:
|
104
|
+
- spec/locker/advisory_spec.rb
|
133
105
|
- spec/locker/locker_spec.rb
|
134
106
|
- spec/spec_helper.rb
|
135
107
|
has_rdoc:
|
data/autotest/discover.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
Autotest.add_discovery { "rspec2" }
|