locker 0.1.0 → 0.2.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
+ data.tar.gz: 5de726ed22ab717d6b7d4c46b78f05ce2d1ffe80
4
+ metadata.gz: 1c082e3e46be1848f8ebaad6a48f9fd640fda707
5
+ SHA512:
6
+ data.tar.gz: 1500e4acd3c15fb9d864bbdbdb30b1fd6d92808d63fe1acd904046ed17f089553706939a54adb7723334eca23e1c298eb7ac66708308de56c0ef82fc5dc372de
7
+ metadata.gz: 91f9f3529ffe477582aed59b861a446134b055e4e293d40daf39596f6ae6c1e2fa5d03209a7145c00126f721f9deb11704bdf7a5ffbc4f4fbe341dacfc62d18f
@@ -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:
@@ -1,3 +1,4 @@
1
1
  require "active_record"
2
2
  require "locker/locker"
3
+ require "locker/advisory"
3
4
  require "locker/version"
@@ -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
@@ -1,3 +1,3 @@
1
1
  class Locker
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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
- hash: 27
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: 2013-04-04 00:00:00 Z
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: &id003 !ruby/object:Gem::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: *id003
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: 1.8.25
99
+ rubygems_version: 2.0.14
129
100
  signing_key:
130
- specification_version: 3
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:
@@ -1 +0,0 @@
1
- Autotest.add_discovery { "rspec2" }