with_advisory_lock 0.0.2 → 0.0.3
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/.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 [](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:
|