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.
@@ -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" }