with_advisory_lock 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -7,13 +7,7 @@ rvm:
7
7
  env:
8
8
  - DB=sqlite
9
9
  - DB=mysql
10
- - DB=pg
11
-
12
- # 1.8.7 and sqlite fails: "SQL statements in progress: rollback transaction"
13
- matrix:
14
- exclude:
15
- - rvm: 1.8.7
16
- env: DB=sqlite
10
+ - DB=postgresql
17
11
 
18
12
  script: bundle exec rake
19
13
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # with_advisory_lock [![Build Status](https://api.travis-ci.org/mceachen/with_advisory_lock.png?branch=master)](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
- ### Gotchas
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
- **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.
47
+ ### MySQL doesn't support nesting
73
48
 
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).
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
@@ -1,3 +1,3 @@
1
1
  module WithAdvisoryLock
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
data/test/database.yml CHANGED
@@ -1,9 +1,9 @@
1
1
  sqlite:
2
2
  adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
3
- database: test/sqlite3.db
3
+ database: test/sqlite.db
4
4
  timeout: 500
5
5
  pool: 50
6
- pg:
6
+ postgresql:
7
7
  adapter: postgresql
8
8
  username: postgres
9
9
  database: with_advisory_lock_test
@@ -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
- ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite")
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
 
@@ -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 if ENV['DB'] == 'mysql'
12
+ end
11
13
  end
@@ -1,34 +1,40 @@
1
1
  require 'minitest_helper'
2
2
 
3
3
  describe "parallelism" do
4
- def find_or_create_at_even_second(run_at, with_advisory_lock)
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 { Tag.find_by_name(name) || Tag.create!(:name => name) }
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
- start_time = Time.now.to_i + 2
18
- threads = @workers.times.collect do
19
- Thread.new do
20
- @iterations.times do |ea|
21
- find_or_create_at_even_second(start_time + (ea * 2), with_advisory_lock)
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 = 10
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
@@ -0,0 +1,13 @@
1
+ #!/bin/sh -e
2
+ export BUNDLE_GEMFILE RMI DB
3
+
4
+ for RMI in 1.8.7-p370 1.9.3-p327
5
+ do
6
+ rbenv local $RMI
7
+ bundle --quiet
8
+ for DB in sqlite mysql postgresql
9
+ do
10
+ echo $DB $BUNDLE_GEMFILE `ruby -v`
11
+ bundle exec rake
12
+ done
13
+ done
@@ -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.0.0'
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.2
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-22 00:00:00.000000000 Z
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.0.0
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.0.0
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: 1854060024724922654
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: 1854060024724922654
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: