with_advisory_lock 0.0.4 → 0.0.5
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/README.md +9 -7
- data/lib/with_advisory_lock/base.rb +20 -2
- data/lib/with_advisory_lock/concern.rb +3 -18
- data/lib/with_advisory_lock/mysql.rb +10 -0
- data/lib/with_advisory_lock/nested_advisory_lock_error.rb +14 -0
- data/lib/with_advisory_lock/version.rb +1 -1
- data/test/concern_test.rb +1 -1
- data/test/minitest_helper.rb +1 -1
- data/test/nesting_test.rb +49 -0
- metadata +7 -6
- data/test/mysql_nesting_test.rb +0 -13
data/README.md
CHANGED
@@ -46,12 +46,10 @@ You will want to wrap your block within a transaction to ensure consistency.
|
|
46
46
|
|
47
47
|
### MySQL doesn't support nesting
|
48
48
|
|
49
|
-
With MySQL, if you ask for
|
50
|
-
you will be releasing the parent lock (!!!).
|
51
|
-
|
52
|
-
|
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!*
|
49
|
+
With MySQL (at least <= v5.5), if you ask for a *different* advisory lock within a ```with_advisory_lock``` block,
|
50
|
+
you will be releasing the parent lock (!!!). A ```NestedAdvisoryLockError```will be raised
|
51
|
+
in this case. If you ask for the same lock name, ```with_advisory_lock``` won't ask for the
|
52
|
+
lock again, and the block given will be yielded to.
|
55
53
|
|
56
54
|
## Installation
|
57
55
|
|
@@ -67,7 +65,6 @@ And then execute:
|
|
67
65
|
|
68
66
|
## Lock Types
|
69
67
|
|
70
|
-
|
71
68
|
First off, know that there are **lots** of different kinds of locks available to you. **Pick the
|
72
69
|
finest-grain lock that ensures correctness.** If you choose a lock that is too coarse, you are
|
73
70
|
unnecessarily blocking other processes.
|
@@ -94,6 +91,11 @@ aren't going to be commonly applicable, and they can be a source of
|
|
94
91
|
|
95
92
|
## Changelog
|
96
93
|
|
94
|
+
### 0.0.5
|
95
|
+
|
96
|
+
* Asking for the currently acquired advisory lock doesn't re-ask for the lock now.
|
97
|
+
* Introduced NestedAdvisoryLockError when asking for different, nested advisory locksMySQL
|
98
|
+
|
97
99
|
### 0.0.4
|
98
100
|
|
99
101
|
* Moved require into on_load, which should speed loading when AR doesn't have to spin up
|
@@ -12,13 +12,31 @@ module WithAdvisoryLock
|
|
12
12
|
connection.quote(lock_name)
|
13
13
|
end
|
14
14
|
|
15
|
-
def
|
15
|
+
def lock_stack
|
16
|
+
Thread.current[:with_advisory_lock_stack] ||= []
|
17
|
+
end
|
18
|
+
|
19
|
+
def already_locked?
|
20
|
+
lock_stack.include? @lock_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_advisory_lock_if_needed
|
24
|
+
if already_locked?
|
25
|
+
yield
|
26
|
+
else
|
27
|
+
yield_with_lock { yield }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def yield_with_lock
|
16
32
|
give_up_at = Time.now + @timeout_seconds if @timeout_seconds
|
17
33
|
while @timeout_seconds.nil? || Time.now < give_up_at do
|
18
34
|
if try_lock
|
19
35
|
begin
|
36
|
+
lock_stack.push(lock_name)
|
20
37
|
return yield
|
21
38
|
ensure
|
39
|
+
lock_stack.pop
|
22
40
|
release_lock
|
23
41
|
end
|
24
42
|
else
|
@@ -30,4 +48,4 @@ module WithAdvisoryLock
|
|
30
48
|
false # failed to get lock in time.
|
31
49
|
end
|
32
50
|
end
|
33
|
-
end
|
51
|
+
end
|
@@ -20,32 +20,17 @@ module WithAdvisoryLock
|
|
20
20
|
module ClassMethods
|
21
21
|
|
22
22
|
def with_advisory_lock(lock_name, timeout_seconds=nil, &block)
|
23
|
-
|
24
|
-
impl = case (connection.adapter_name.downcase)
|
23
|
+
impl_class = case (connection.adapter_name.downcase)
|
25
24
|
when "postgresql"
|
26
25
|
WithAdvisoryLock::PostgreSQL
|
27
26
|
when "mysql", "mysql2"
|
28
|
-
unless lock_stack.empty?
|
29
|
-
wal_log("with_advisory_lock: MySQL doesn't support nested advisory locks, and will now release lock '#{lock_stack.last}'")
|
30
|
-
end
|
31
27
|
WithAdvisoryLock::MySQL
|
32
28
|
else
|
33
29
|
WithAdvisoryLock::Flock
|
34
30
|
end
|
35
|
-
|
36
|
-
impl.
|
37
|
-
ensure
|
38
|
-
lock_stack.pop
|
31
|
+
impl = impl_class.new(connection, lock_name, timeout_seconds)
|
32
|
+
impl.with_advisory_lock_if_needed(&block)
|
39
33
|
end
|
40
|
-
|
41
|
-
def wal_log(msg)
|
42
|
-
if respond_to?(:logger) && logger
|
43
|
-
logger.warn(msg)
|
44
|
-
else
|
45
|
-
$stderr.puts(msg)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
private :wal_log
|
49
34
|
end
|
50
35
|
end
|
51
36
|
end
|
@@ -1,9 +1,15 @@
|
|
1
|
+
require 'with_advisory_lock/nested_advisory_lock_error'
|
1
2
|
module WithAdvisoryLock
|
2
3
|
class MySQL < Base
|
3
4
|
|
4
5
|
# See http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock
|
5
6
|
|
6
7
|
def try_lock
|
8
|
+
unless lock_stack.empty?
|
9
|
+
raise NestedAdvisoryLockError.new(
|
10
|
+
"MySQL doesn't support nested Advisory Locks",
|
11
|
+
lock_stack)
|
12
|
+
end
|
7
13
|
# Returns 1 if the lock was obtained successfully,
|
8
14
|
# 0 if the attempt timed out (for example, because another client has
|
9
15
|
# previously locked the name), or NULL if an error occurred
|
@@ -18,5 +24,9 @@ module WithAdvisoryLock
|
|
18
24
|
# NULL if the named lock did not exist.
|
19
25
|
1 == connection.select_value("SELECT RELEASE_LOCK(#{quoted_lock_name})")
|
20
26
|
end
|
27
|
+
|
28
|
+
def already_locked?
|
29
|
+
lock_stack.last == @lock_name
|
30
|
+
end
|
21
31
|
end
|
22
32
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module WithAdvisoryLock
|
2
|
+
class NestedAdvisoryLockError < StandardError
|
3
|
+
attr_accessor :lock_stack
|
4
|
+
|
5
|
+
def initialize(msg = nil, lock_stack = nil)
|
6
|
+
super(msg)
|
7
|
+
@lock_stack = lock_stack
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
super + (lock_stack ? ": lock stack = #{lock_stack}" : "")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/test/concern_test.rb
CHANGED
data/test/minitest_helper.rb
CHANGED
@@ -8,7 +8,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
10
|
def env_db
|
11
|
-
ENV["DB"] || "
|
11
|
+
ENV["DB"] || "sqlite"
|
12
12
|
end
|
13
13
|
|
14
14
|
ActiveRecord::Base.establish_connection(env_db)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'minitest_helper'
|
2
|
+
|
3
|
+
describe "lock nesting" do
|
4
|
+
it "doesn't request the same lock twice" do
|
5
|
+
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
6
|
+
impl.lock_stack.must_be_empty
|
7
|
+
Tag.with_advisory_lock("first") do
|
8
|
+
impl.lock_stack.must_equal %w(first)
|
9
|
+
# Even MySQL should be OK with this:
|
10
|
+
Tag.with_advisory_lock("first") do
|
11
|
+
impl.lock_stack.must_equal %w(first)
|
12
|
+
end
|
13
|
+
impl.lock_stack.must_equal %w(first)
|
14
|
+
end
|
15
|
+
impl.lock_stack.must_be_empty
|
16
|
+
end
|
17
|
+
|
18
|
+
it "raises errors with MySQL when acquiring nested lock" do
|
19
|
+
skip if env_db != 'mysql'
|
20
|
+
proc {
|
21
|
+
Tag.with_advisory_lock("first") do
|
22
|
+
Tag.with_advisory_lock("second") do
|
23
|
+
end
|
24
|
+
end
|
25
|
+
}.must_raise WithAdvisoryLock::NestedAdvisoryLockError
|
26
|
+
end
|
27
|
+
|
28
|
+
it "supports nested advisory locks with !MySQL" do
|
29
|
+
skip if env_db == 'mysql'
|
30
|
+
impl = WithAdvisoryLock::Base.new(nil, nil, nil)
|
31
|
+
impl.lock_stack.must_be_empty
|
32
|
+
Tag.with_advisory_lock("first") do
|
33
|
+
impl.lock_stack.must_equal %w(first)
|
34
|
+
Tag.with_advisory_lock("second") do
|
35
|
+
impl.lock_stack.must_equal %w(first second)
|
36
|
+
Tag.with_advisory_lock("first") do
|
37
|
+
# Shouldn't ask for another lock:
|
38
|
+
impl.lock_stack.must_equal %w(first second)
|
39
|
+
Tag.with_advisory_lock("second") do
|
40
|
+
# Shouldn't ask for another lock:
|
41
|
+
impl.lock_stack.must_equal %w(first second)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
impl.lock_stack.must_equal %w(first)
|
46
|
+
end
|
47
|
+
impl.lock_stack.must_be_empty
|
48
|
+
end
|
49
|
+
end
|
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.5
|
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-
|
12
|
+
date: 2013-02-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -189,12 +189,13 @@ files:
|
|
189
189
|
- lib/with_advisory_lock/concern.rb
|
190
190
|
- lib/with_advisory_lock/flock.rb
|
191
191
|
- lib/with_advisory_lock/mysql.rb
|
192
|
+
- lib/with_advisory_lock/nested_advisory_lock_error.rb
|
192
193
|
- lib/with_advisory_lock/postgresql.rb
|
193
194
|
- lib/with_advisory_lock/version.rb
|
194
195
|
- test/concern_test.rb
|
195
196
|
- test/database.yml
|
196
197
|
- test/minitest_helper.rb
|
197
|
-
- test/
|
198
|
+
- test/nesting_test.rb
|
198
199
|
- test/parallelism_test.rb
|
199
200
|
- test/simplest_test.rb
|
200
201
|
- test/test_models.rb
|
@@ -214,7 +215,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
214
215
|
version: '0'
|
215
216
|
segments:
|
216
217
|
- 0
|
217
|
-
hash: -
|
218
|
+
hash: -1188809411944032990
|
218
219
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
219
220
|
none: false
|
220
221
|
requirements:
|
@@ -223,7 +224,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
223
224
|
version: '0'
|
224
225
|
segments:
|
225
226
|
- 0
|
226
|
-
hash: -
|
227
|
+
hash: -1188809411944032990
|
227
228
|
requirements: []
|
228
229
|
rubyforge_project:
|
229
230
|
rubygems_version: 1.8.23
|
@@ -234,7 +235,7 @@ test_files:
|
|
234
235
|
- test/concern_test.rb
|
235
236
|
- test/database.yml
|
236
237
|
- test/minitest_helper.rb
|
237
|
-
- test/
|
238
|
+
- test/nesting_test.rb
|
238
239
|
- test/parallelism_test.rb
|
239
240
|
- test/simplest_test.rb
|
240
241
|
- test/test_models.rb
|
data/test/mysql_nesting_test.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
require 'minitest_helper'
|
2
|
-
|
3
|
-
describe "lock nesting" do
|
4
|
-
it "warns about MySQL releasing advisory locks" do
|
5
|
-
skip if env_db != 'mysql'
|
6
|
-
|
7
|
-
Tag.expects(:wal_log)
|
8
|
-
Tag.with_advisory_lock("first") do
|
9
|
-
Tag.with_advisory_lock("second") do
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|