with_advisory_lock 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -66,6 +66,15 @@ The return value of ```with_advisory_lock``` will be the result of the yielded b
66
66
  if the lock was able to be acquired and the block yielded, or ```false```, if you provided
67
67
  a timeout_seconds value and the lock was not able to be acquired in time.
68
68
 
69
+ ### Gotchas
70
+
71
+ **MySQL doesn't support nesting advisory locks.** If you ask for another advisory lock within
72
+ a ```with_advisory_lock``` block, you will be releasing the parent lock.
73
+
74
+ An warning message will be emitted to the rails logger in this case, because you
75
+ probably didn't mean to lose the first lock. (Raising an exception would be safer. I'm open to
76
+ suggestions on how to handle this dangerous case).
77
+
69
78
  ## Installation
70
79
 
71
80
  Add this line to your application's Gemfile:
@@ -80,6 +89,11 @@ And then execute:
80
89
 
81
90
  ## Changelog
82
91
 
92
+ ### 0.0.2
93
+
94
+ * Added warning log message for nested MySQL lock calls
95
+ * Randomized lock wait time, which can help ameliorate lock contention
96
+
83
97
  ### 0.0.1
84
98
 
85
99
  * First whack
@@ -22,7 +22,9 @@ module WithAdvisoryLock
22
22
  release_lock
23
23
  end
24
24
  else
25
- sleep(0.1)
25
+ # sleep between 1/20 and ~1/5 of a second.
26
+ # Randomizing sleep time may help reduce contention.
27
+ sleep(rand * 0.15 + 0.05)
26
28
  end
27
29
  end
28
30
  false # failed to get lock in time.
@@ -18,16 +18,34 @@ module WithAdvisoryLock
18
18
  end
19
19
 
20
20
  module ClassMethods
21
+
21
22
  def with_advisory_lock(lock_name, timeout_seconds=nil, &block)
22
- case (connection.adapter_name.downcase)
23
+ lock_stack = Thread.current[:with_advisory_lock_stack] ||= []
24
+ impl = case (connection.adapter_name.downcase)
23
25
  when "postgresql"
24
26
  WithAdvisoryLock::PostgreSQL
25
27
  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
26
31
  WithAdvisoryLock::MySQL
27
32
  else
28
33
  WithAdvisoryLock::Flock
29
- end.new(connection, lock_name, timeout_seconds).with_advisory_lock(&block)
34
+ 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
39
+ 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
30
47
  end
48
+ private :wal_log
31
49
  end
32
50
  end
33
51
  end
@@ -1,3 +1,3 @@
1
1
  module WithAdvisoryLock
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,11 @@
1
+ require 'minitest_helper'
2
+
3
+ describe "with_advisory_lock.concern" do
4
+ it "adds with_advisory_lock to ActiveRecord classes" do
5
+ assert Tag.respond_to?(:with_advisory_lock)
6
+ end
7
+
8
+ it "adds with_advisory_lock to ActiveRecord instances" do
9
+ assert Tag.new.respond_to?(:with_advisory_lock)
10
+ end
11
+ end
@@ -12,6 +12,7 @@ ActiveRecord::Migration.verbose = false
12
12
  require 'test_models'
13
13
  require 'minitest/autorun'
14
14
  require 'minitest/great_expectations'
15
+ require 'mocha/setup'
15
16
 
16
17
  Thread.abort_on_exception = true
17
18
 
@@ -0,0 +1,11 @@
1
+ require 'minitest_helper'
2
+
3
+ describe "lock nesting" do
4
+ it "warns about MySQL releasing advisory locks" do
5
+ Tag.expects(:wal_log)
6
+ Tag.with_advisory_lock("first") do
7
+ Tag.with_advisory_lock("second") do
8
+ end
9
+ end
10
+ end if ENV['DB'] == 'mysql'
11
+ end
@@ -1,14 +1,6 @@
1
1
  require 'minitest_helper'
2
2
 
3
- describe "with_advisory_lock" do
4
- it "adds with_advisory_lock to ActiveRecord classes" do
5
- assert Tag.respond_to?(:with_advisory_lock)
6
- end
7
-
8
- it "adds with_advisory_lock to ActiveRecord instances" do
9
- assert Tag.new.respond_to?(:with_advisory_lock)
10
- end
11
-
3
+ describe "parallelism" do
12
4
  def find_or_create_at_even_second(run_at, with_advisory_lock)
13
5
  sleep(run_at - Time.now.to_f)
14
6
  ActiveRecord::Base.connection.reconnect!
@@ -36,7 +28,7 @@ describe "with_advisory_lock" do
36
28
 
37
29
  before :each do
38
30
  @iterations = 5
39
- @workers = 7
31
+ @workers = 10
40
32
  end
41
33
 
42
34
  it "parallel threads create multiple duplicate rows" do
@@ -23,6 +23,7 @@ Gem::Specification.new do |gem|
23
23
  gem.add_development_dependency 'yard'
24
24
  gem.add_development_dependency 'minitest'
25
25
  gem.add_development_dependency 'minitest-great_expectations'
26
+ gem.add_development_dependency 'mocha'
26
27
  gem.add_development_dependency 'mysql2'
27
28
  gem.add_development_dependency 'pg'
28
29
  gem.add_development_dependency 'sqlite3'
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.1
4
+ version: 0.0.2
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-20 00:00:00.000000000 Z
12
+ date: 2013-01-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -91,6 +91,22 @@ dependencies:
91
91
  - - ! '>='
92
92
  - !ruby/object:Gem::Version
93
93
  version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: mocha
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
94
110
  - !ruby/object:Gem::Dependency
95
111
  name: mysql2
96
112
  requirement: !ruby/object:Gem::Requirement
@@ -175,10 +191,12 @@ files:
175
191
  - lib/with_advisory_lock/mysql.rb
176
192
  - lib/with_advisory_lock/postgresql.rb
177
193
  - lib/with_advisory_lock/version.rb
194
+ - test/concern_test.rb
178
195
  - test/database.yml
179
196
  - test/minitest_helper.rb
197
+ - test/mysql_nesting_test.rb
198
+ - test/parallelism_test.rb
180
199
  - test/test_models.rb
181
- - test/with_advisory_lock_test.rb
182
200
  - with_advisory_lock.gemspec
183
201
  homepage: ''
184
202
  licenses: []
@@ -194,7 +212,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
212
  version: '0'
195
213
  segments:
196
214
  - 0
197
- hash: 2709177541654258140
215
+ hash: 1854060024724922654
198
216
  required_rubygems_version: !ruby/object:Gem::Requirement
199
217
  none: false
200
218
  requirements:
@@ -203,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
203
221
  version: '0'
204
222
  segments:
205
223
  - 0
206
- hash: 2709177541654258140
224
+ hash: 1854060024724922654
207
225
  requirements: []
208
226
  rubyforge_project:
209
227
  rubygems_version: 1.8.23
@@ -211,8 +229,10 @@ signing_key:
211
229
  specification_version: 3
212
230
  summary: Advisory locking for ActiveRecord
213
231
  test_files:
232
+ - test/concern_test.rb
214
233
  - test/database.yml
215
234
  - test/minitest_helper.rb
235
+ - test/mysql_nesting_test.rb
236
+ - test/parallelism_test.rb
216
237
  - test/test_models.rb
217
- - test/with_advisory_lock_test.rb
218
238
  has_rdoc: