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 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