with_advisory_lock 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -7
- data/README.md +45 -35
- data/lib/with_advisory_lock/version.rb +1 -1
- data/test/database.yml +2 -2
- data/test/minitest_helper.rb +7 -2
- data/test/mysql_nesting_test.rb +3 -1
- data/test/parallelism_test.rb +16 -10
- data/test/simplest_test.rb +29 -0
- data/tests.sh +13 -0
- data/with_advisory_lock.gemspec +1 -1
- metadata +9 -6
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# with_advisory_lock [![Build Status](https://api.travis-ci.org/mceachen/with_advisory_lock.png?branch=master)](https://travis-ci.org/mceachen/with_advisory_lock)
|
2
2
|
|
3
|
-
Adds advisory locking to ActiveRecord 3.x.
|
3
|
+
Adds advisory locking to ActiveRecord 3.2.x.
|
4
4
|
[MySQL](http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock)
|
5
5
|
and [PostgreSQL](http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS)
|
6
6
|
are supported natively. SQLite resorts to file locking (which won't span hosts, of course!).
|
@@ -11,34 +11,6 @@ An advisory lock is a [mutex](http://en.wikipedia.org/wiki/Mutual_exclusion) use
|
|
11
11
|
processes run some process at the same time. When the advisory lock is powered by your database
|
12
12
|
server, as long as it isn't SQLite, your mutex spans hosts.
|
13
13
|
|
14
|
-
Advisory locks ignore database transaction boundaries.
|
15
|
-
|
16
|
-
## Lock Types
|
17
|
-
|
18
|
-
First off, know that there are **lots** of different kinds of locks available to you. You want the
|
19
|
-
finest-grain lock that ensures correctness. If you choose a lock that is too coarse, you are
|
20
|
-
unnecessarily blocking other processes.
|
21
|
-
|
22
|
-
### Row-level locks
|
23
|
-
Whether [optimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html)
|
24
|
-
or [pessimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html),
|
25
|
-
row-level locks prevent concurrent modification to a given model.
|
26
|
-
|
27
|
-
**If you're building a
|
28
|
-
[CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) application, this will be your
|
29
|
-
most commonly used lock.**
|
30
|
-
|
31
|
-
### Advisory locks
|
32
|
-
|
33
|
-
These are named mutexes that are inherently "application level"—it is up to the application
|
34
|
-
to acquire, run a critical code section, and release the advisory lock.
|
35
|
-
|
36
|
-
### Table-level locks
|
37
|
-
|
38
|
-
Provided through something like the [monogamy](https://github.com/mceachen/monogamy)
|
39
|
-
gem, these prevent concurrent access to **any instance of a model**. You probably don't want these,
|
40
|
-
and they can be a source of [deadlocks](http://en.wikipedia.org/wiki/Deadlock).
|
41
|
-
|
42
14
|
## Usage
|
43
15
|
|
44
16
|
Where ```User``` is an ActiveRecord model, and ```lock_name``` is some string:
|
@@ -66,14 +38,20 @@ The return value of ```with_advisory_lock``` will be the result of the yielded b
|
|
66
38
|
if the lock was able to be acquired and the block yielded, or ```false```, if you provided
|
67
39
|
a timeout_seconds value and the lock was not able to be acquired in time.
|
68
40
|
|
69
|
-
###
|
41
|
+
### Transactions and Advisory Locks
|
42
|
+
|
43
|
+
Advisory locks with MySQL and PostgreSQL ignore database transaction boundaries.
|
44
|
+
|
45
|
+
You will want to wrap your block within a transaction to ensure consistency.
|
70
46
|
|
71
|
-
|
72
|
-
a ```with_advisory_lock``` block, you will be releasing the parent lock.
|
47
|
+
### MySQL doesn't support nesting
|
73
48
|
|
74
|
-
|
75
|
-
|
76
|
-
|
49
|
+
With MySQL, if you ask for another advisory lock within a ```with_advisory_lock``` block,
|
50
|
+
you will be releasing the parent lock (!!!).
|
51
|
+
|
52
|
+
A warning message will be emitted to the Rails logger in this case, because you
|
53
|
+
probably didn't mean to lose your first lock. *Raising an exception would be safer. I'm open to
|
54
|
+
suggestions on how to handle this dangerous case!*
|
77
55
|
|
78
56
|
## Installation
|
79
57
|
|
@@ -87,8 +65,40 @@ And then execute:
|
|
87
65
|
|
88
66
|
$ bundle
|
89
67
|
|
68
|
+
## Lock Types
|
69
|
+
|
70
|
+
|
71
|
+
First off, know that there are **lots** of different kinds of locks available to you. **Pick the
|
72
|
+
finest-grain lock that ensures correctness.** If you choose a lock that is too coarse, you are
|
73
|
+
unnecessarily blocking other processes.
|
74
|
+
|
75
|
+
### Advisory locks
|
76
|
+
These are named mutexes that are inherently "application level"—it is up to the application
|
77
|
+
to acquire, run a critical code section, and release the advisory lock.
|
78
|
+
|
79
|
+
### Row-level locks
|
80
|
+
Whether [optimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html)
|
81
|
+
or [pessimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html),
|
82
|
+
row-level locks prevent concurrent modification to a given model.
|
83
|
+
|
84
|
+
**If you're building a
|
85
|
+
[CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) application, this will be your
|
86
|
+
most commonly used lock.**
|
87
|
+
|
88
|
+
### Table-level locks
|
89
|
+
|
90
|
+
Provided through something like the [monogamy](https://github.com/mceachen/monogamy)
|
91
|
+
gem, these prevent concurrent access to **any instance of a model**. Their coarseness means they
|
92
|
+
aren't going to be commonly applicable, and they can be a source of
|
93
|
+
[deadlocks](http://en.wikipedia.org/wiki/Deadlock).
|
94
|
+
|
90
95
|
## Changelog
|
91
96
|
|
97
|
+
### 0.0.3
|
98
|
+
|
99
|
+
* Fought with ActiveRecord 3.0.x and 3.1.x. You don't want them if you use threads—they fail
|
100
|
+
predictably.
|
101
|
+
|
92
102
|
### 0.0.2
|
93
103
|
|
94
104
|
* Added warning log message for nested MySQL lock calls
|
data/test/database.yml
CHANGED
data/test/minitest_helper.rb
CHANGED
@@ -6,7 +6,12 @@ require 'tmpdir'
|
|
6
6
|
|
7
7
|
db_config = File.expand_path("database.yml", File.dirname(__FILE__))
|
8
8
|
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result)
|
9
|
-
|
9
|
+
|
10
|
+
def env_db
|
11
|
+
ENV["DB"] || "sqlite3"
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveRecord::Base.establish_connection(env_db)
|
10
15
|
ActiveRecord::Migration.verbose = false
|
11
16
|
|
12
17
|
require 'test_models'
|
@@ -23,8 +28,8 @@ class MiniTest::Spec
|
|
23
28
|
DatabaseCleaner.start
|
24
29
|
end
|
25
30
|
after do
|
26
|
-
FileUtils.remove_entry_secure ENV['FLOCK_DIR']
|
27
31
|
DatabaseCleaner.clean
|
32
|
+
FileUtils.remove_entry_secure ENV['FLOCK_DIR']
|
28
33
|
end
|
29
34
|
end
|
30
35
|
|
data/test/mysql_nesting_test.rb
CHANGED
@@ -2,10 +2,12 @@ require 'minitest_helper'
|
|
2
2
|
|
3
3
|
describe "lock nesting" do
|
4
4
|
it "warns about MySQL releasing advisory locks" do
|
5
|
+
skip if env_db != 'mysql'
|
6
|
+
|
5
7
|
Tag.expects(:wal_log)
|
6
8
|
Tag.with_advisory_lock("first") do
|
7
9
|
Tag.with_advisory_lock("second") do
|
8
10
|
end
|
9
11
|
end
|
10
|
-
end
|
12
|
+
end
|
11
13
|
end
|
data/test/parallelism_test.rb
CHANGED
@@ -1,34 +1,40 @@
|
|
1
1
|
require 'minitest_helper'
|
2
2
|
|
3
3
|
describe "parallelism" do
|
4
|
-
def
|
5
|
-
sleep(run_at - Time.now.to_f)
|
4
|
+
def find_or_create_at(run_at, with_advisory_lock)
|
6
5
|
ActiveRecord::Base.connection.reconnect!
|
6
|
+
sleep(run_at - Time.now.to_f)
|
7
7
|
name = run_at.to_s
|
8
|
-
task = lambda
|
8
|
+
task = lambda do
|
9
|
+
Tag.transaction do
|
10
|
+
Tag.find_by_name(name) || Tag.create(:name => name)
|
11
|
+
end
|
12
|
+
end
|
9
13
|
if with_advisory_lock
|
10
14
|
Tag.with_advisory_lock(name, nil, &task)
|
11
15
|
else
|
12
16
|
task.call
|
13
17
|
end
|
18
|
+
ActiveRecord::Base.connection.close
|
14
19
|
end
|
15
20
|
|
16
21
|
def run_workers(with_advisory_lock)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
skip if env_db == "sqlite"
|
23
|
+
@iterations.times do
|
24
|
+
time = (Time.now.to_i + 2).to_f
|
25
|
+
threads = @workers.times.collect do
|
26
|
+
Thread.new do
|
27
|
+
find_or_create_at(time, with_advisory_lock)
|
22
28
|
end
|
23
29
|
end
|
30
|
+
threads.each { |ea| ea.join }
|
24
31
|
end
|
25
|
-
threads.each { |ea| ea.join }
|
26
32
|
puts "Created #{Tag.all.size} (lock = #{with_advisory_lock})"
|
27
33
|
end
|
28
34
|
|
29
35
|
before :each do
|
30
36
|
@iterations = 5
|
31
|
-
@workers =
|
37
|
+
@workers = 5
|
32
38
|
end
|
33
39
|
|
34
40
|
it "parallel threads create multiple duplicate rows" do
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe "simplest" do
|
4
|
+
it "should prevent threads from accessing a resource concurrently" do
|
5
|
+
times = ActiveSupport::OrderedHash.new
|
6
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
7
|
+
t1 = Thread.new do
|
8
|
+
ActiveRecord::Base.connection.reconnect!
|
9
|
+
ActiveRecord::Base.with_advisory_lock("simplest test") do
|
10
|
+
times[:t1_acquire] = Time.now
|
11
|
+
sleep 0.5
|
12
|
+
end
|
13
|
+
times[:t1_release] = Time.now
|
14
|
+
end
|
15
|
+
sleep 0.1
|
16
|
+
t2 = Thread.new do
|
17
|
+
ActiveRecord::Base.connection.reconnect!
|
18
|
+
ActiveRecord::Base.with_advisory_lock("simplest test") do
|
19
|
+
times[:t2_acquire] = Time.now
|
20
|
+
sleep 1
|
21
|
+
end
|
22
|
+
times[:t2_release] = Time.now
|
23
|
+
end
|
24
|
+
t1.join
|
25
|
+
t2.join
|
26
|
+
times.keys.must_equal [:t1_acquire, :t1_release, :t2_acquire, :t2_release]
|
27
|
+
times[:t2_acquire].must_be :>, times[:t1_release]
|
28
|
+
end
|
29
|
+
end
|
data/tests.sh
ADDED
data/with_advisory_lock.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^test/})
|
18
18
|
gem.require_paths = %w(lib)
|
19
19
|
|
20
|
-
gem.add_runtime_dependency 'activerecord', '>= 3.
|
20
|
+
gem.add_runtime_dependency 'activerecord', '>= 3.2.0'
|
21
21
|
|
22
22
|
gem.add_development_dependency 'rake'
|
23
23
|
gem.add_development_dependency 'yard'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: with_advisory_lock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -18,7 +18,7 @@ dependencies:
|
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 3.
|
21
|
+
version: 3.2.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ! '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 3.
|
29
|
+
version: 3.2.0
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: rake
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -196,7 +196,9 @@ files:
|
|
196
196
|
- test/minitest_helper.rb
|
197
197
|
- test/mysql_nesting_test.rb
|
198
198
|
- test/parallelism_test.rb
|
199
|
+
- test/simplest_test.rb
|
199
200
|
- test/test_models.rb
|
201
|
+
- tests.sh
|
200
202
|
- with_advisory_lock.gemspec
|
201
203
|
homepage: ''
|
202
204
|
licenses: []
|
@@ -212,7 +214,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
212
214
|
version: '0'
|
213
215
|
segments:
|
214
216
|
- 0
|
215
|
-
hash:
|
217
|
+
hash: 1855409000394633980
|
216
218
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
217
219
|
none: false
|
218
220
|
requirements:
|
@@ -221,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
221
223
|
version: '0'
|
222
224
|
segments:
|
223
225
|
- 0
|
224
|
-
hash:
|
226
|
+
hash: 1855409000394633980
|
225
227
|
requirements: []
|
226
228
|
rubyforge_project:
|
227
229
|
rubygems_version: 1.8.23
|
@@ -234,5 +236,6 @@ test_files:
|
|
234
236
|
- test/minitest_helper.rb
|
235
237
|
- test/mysql_nesting_test.rb
|
236
238
|
- test/parallelism_test.rb
|
239
|
+
- test/simplest_test.rb
|
237
240
|
- test/test_models.rb
|
238
241
|
has_rdoc:
|