with_advisory_lock 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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 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!*
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 with_advisory_lock(&block)
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
- lock_stack = Thread.current[:with_advisory_lock_stack] ||= []
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
- lock_stack.push(lock_name)
36
- impl.new(connection, lock_name, timeout_seconds).with_advisory_lock(&block)
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
@@ -1,3 +1,3 @@
1
1
  module WithAdvisoryLock
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
@@ -6,6 +6,6 @@ describe "with_advisory_lock.concern" do
6
6
  end
7
7
 
8
8
  it "adds with_advisory_lock to ActiveRecord instances" do
9
- assert Tag.new.respond_to?(:with_advisory_lock)
9
+ assert Label.new.respond_to?(:with_advisory_lock)
10
10
  end
11
11
  end
@@ -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"] || "sqlite3"
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
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-01-28 00:00:00.000000000 Z
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/mysql_nesting_test.rb
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: -2231625769835238034
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: -2231625769835238034
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/mysql_nesting_test.rb
238
+ - test/nesting_test.rb
238
239
  - test/parallelism_test.rb
239
240
  - test/simplest_test.rb
240
241
  - test/test_models.rb
@@ -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