activerecord-tablelocks 0.0.1
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/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +56 -0
- data/Rakefile +1 -0
- data/activerecord-tablelocks.gemspec +27 -0
- data/lib/activerecord-tablelocks.rb +9 -0
- data/lib/activerecord/tablelocks/activerecord.rb +56 -0
- data/lib/activerecord/tablelocks/activerecord/postgres.rb +14 -0
- data/lib/activerecord/tablelocks/railtie.rb +10 -0
- data/lib/activerecord/tablelocks/version.rb +5 -0
- data/spec/extension_methods_spec.rb +27 -0
- data/spec/locking_targets_spec.rb +39 -0
- data/spec/models/comment.rb +30 -0
- data/spec/models/group.rb +22 -0
- data/spec/models/page.rb +3 -0
- data/spec/models/user.rb +18 -0
- data/spec/normal_behaviour_spec.rb +9 -0
- data/spec/race_conditions_spec.rb +208 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/atomic_integer.rb +14 -0
- data/spec/support/database.rb +39 -0
- data/spec/support/random.rb +6 -0
- metadata +170 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sernin van de Krol
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
## ActiveRecord Tablelocks
|
2
|
+
|
3
|
+
This gem adds table locking functionality to ActiveRecord.
|
4
|
+
Every save and destroy action is already done using a transaction, but now you can add locking to this process to make sure you really have unique records.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'activerecord-tablelocks'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install activerecord-tablelocks
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
In your models use the following to lock the table when editing records:
|
23
|
+
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
validates :login, :uniqueness => true, :presence => true
|
26
|
+
# The following line enables locking to make sure the uniqueness constraint holds.
|
27
|
+
enable_locking
|
28
|
+
end
|
29
|
+
|
30
|
+
If you need to lock multiple table, you can specify this in two ways.
|
31
|
+
|
32
|
+
Using class names:
|
33
|
+
|
34
|
+
class EmailBox < ActiveRecord::Base
|
35
|
+
enable_locking :class_names => ['EmailAlias']
|
36
|
+
# The class name needs to be a String, but doesn't need to be in an Array.
|
37
|
+
# Don't forget to add validations, this gem doesn't do that for you.
|
38
|
+
end
|
39
|
+
|
40
|
+
Using table names:
|
41
|
+
|
42
|
+
class EmailAlias < ActiveRecord::Base
|
43
|
+
enable_locking :table_names => ['email_boxes']
|
44
|
+
# The table name needs to be a String, but doesn't need to be in an Array.
|
45
|
+
# The gem quotes the table name for you, so you don't need to that here.
|
46
|
+
# Don't forget to add validations, this gem doesn't do that for you.
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
## Contributing
|
51
|
+
|
52
|
+
1. Fork it
|
53
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
54
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
55
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
56
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activerecord/tablelocks/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "activerecord-tablelocks"
|
8
|
+
spec.version = Activerecord::Tablelocks::VERSION
|
9
|
+
spec.authors = ["Sernin van de Krol"]
|
10
|
+
spec.email = ["serninpc@paneidos.net"]
|
11
|
+
spec.description = %q{This gem enables the use of database specific table locks when saving or destroying your ActiveRecord objects. This ensures no race conditions exist when using e.g. validates_uniqueness_of.}
|
12
|
+
spec.summary = %q{Use native table locks of your database for your ActiveRecord models}
|
13
|
+
spec.homepage = "https://github.com/paneidos/activerecord-tablelocks"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split("\n")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "activerecord", ">= 3.2.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", ">= 1.2"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "railties"
|
26
|
+
spec.add_development_dependency "rspec", "~> 2.14.1"
|
27
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Extends AR to add table locking
|
2
|
+
module ActiveRecord
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
attr_reader :locking_enabled
|
6
|
+
|
7
|
+
def enable_locking(options = {})
|
8
|
+
@locking_enabled = true
|
9
|
+
@lock_targets = {
|
10
|
+
:class_names => [*options[:class_names]].compact,
|
11
|
+
:table_names => [*options[:table_names]].compact
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def lock_targets
|
16
|
+
@lock_targets ||= {
|
17
|
+
:class_names => [],
|
18
|
+
:table_names => []
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def tables_to_lock
|
23
|
+
return [] unless @locking_enabled
|
24
|
+
[quoted_table_name,
|
25
|
+
*@lock_targets[:table_names].map{|table_name| connection.quote_table_name(table_name) },
|
26
|
+
*@lock_targets[:class_names].map{|class_name| class_name.constantize.quoted_table_name }].sort.uniq
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_to_transaction
|
31
|
+
if self.class.locking_enabled
|
32
|
+
self.class.connection.lock_tables self.class.tables_to_lock
|
33
|
+
end
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
module ConnectionAdapters
|
38
|
+
class AbstractAdapter
|
39
|
+
def self.inherited(subclass)
|
40
|
+
case subclass.name
|
41
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
42
|
+
require 'activerecord/tablelocks/activerecord/postgres'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
def lock_table(quoted_table_name)
|
46
|
+
logger.warn "WARNING: Locking is not supported for your database!"
|
47
|
+
end
|
48
|
+
def lock_tables(quoted_table_names)
|
49
|
+
logger.warn "WARNING: Locking is not supported for your database!"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
if defined?(PostgreSQLAdapter)
|
53
|
+
require 'activerecord/tablelocks/activerecord/postgres'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class PostgreSQLAdapter
|
4
|
+
def lock_table(quoted_table_name)
|
5
|
+
execute "LOCK TABLE #{quoted_table_name} IN EXCLUSIVE MODE"
|
6
|
+
end
|
7
|
+
def lock_tables(quoted_table_names)
|
8
|
+
quoted_table_names.each do |quoted_table_name|
|
9
|
+
lock_table quoted_table_name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe "Extension methods" do
|
3
|
+
describe "ActiveRecord objects" do
|
4
|
+
it "should accept zero arguments for 'enable_locking'" do
|
5
|
+
klass = Class.new(ActiveRecord::Base)
|
6
|
+
lambda { klass.enable_locking }.should_not raise_error
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should accept a hash with options" do
|
10
|
+
klass = Class.new(ActiveRecord::Base)
|
11
|
+
lambda { klass.enable_locking({}) }.should_not raise_error
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should have an empty list at the start" do
|
15
|
+
klass = Class.new(ActiveRecord::Base)
|
16
|
+
klass.lock_targets[:class_names].should == []
|
17
|
+
klass.lock_targets[:table_names].should == []
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should save class_names and table_names from options" do
|
21
|
+
klass = Class.new(ActiveRecord::Base)
|
22
|
+
klass.enable_locking({ class_names: ['User', 'Group'], table_names: ['users_groups'] })
|
23
|
+
klass.lock_targets[:class_names].sort.should == ['Group','User']
|
24
|
+
klass.lock_targets[:table_names].sort.should == ['users_groups']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'models/page'
|
3
|
+
require 'models/comment'
|
4
|
+
require 'models/user'
|
5
|
+
require 'models/group'
|
6
|
+
|
7
|
+
describe "Locking target" do
|
8
|
+
it "should be empty by default" do
|
9
|
+
Page.tables_to_lock.should == []
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should include the class' own table by default" do
|
13
|
+
Comment.tables_to_lock.should include(Comment.quoted_table_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should include all specified classes" do
|
17
|
+
User.tables_to_lock.should include(User.quoted_table_name)
|
18
|
+
User.tables_to_lock.should include(Group.quoted_table_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should include all specified tables" do
|
22
|
+
Group.tables_to_lock.should include(User.quoted_table_name)
|
23
|
+
Group.tables_to_lock.should include(Group.quoted_table_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should have the tables to lock sorted" do
|
27
|
+
User.tables_to_lock.sort.should == User.tables_to_lock
|
28
|
+
Comment.tables_to_lock.sort.should == Comment.tables_to_lock
|
29
|
+
Page.tables_to_lock.sort.should == Page.tables_to_lock
|
30
|
+
Group.tables_to_lock.sort.should == Group.tables_to_lock
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should not contain duplicate table names" do
|
34
|
+
User.tables_to_lock.uniq.should == User.tables_to_lock
|
35
|
+
Comment.tables_to_lock.uniq.should == Comment.tables_to_lock
|
36
|
+
Page.tables_to_lock.uniq.should == Page.tables_to_lock
|
37
|
+
Group.tables_to_lock.uniq.should == Group.tables_to_lock
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Comment < ActiveRecord::Base
|
2
|
+
enable_locking
|
3
|
+
validates :title, :uniqueness => true, :presence => true
|
4
|
+
|
5
|
+
|
6
|
+
attr_accessor :validate_mutexes
|
7
|
+
attr_accessor :commit_mutexes
|
8
|
+
attr_accessor :wait_time
|
9
|
+
after_validation :release_mutexes
|
10
|
+
after_validation :wait
|
11
|
+
|
12
|
+
def wait
|
13
|
+
if wait_time.present?
|
14
|
+
sleep wait_time
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def release_mutexes
|
19
|
+
if validate_mutexes.present?
|
20
|
+
validate_mutexes.each do |mutex|
|
21
|
+
mutex.unlock
|
22
|
+
end
|
23
|
+
end
|
24
|
+
if commit_mutexes.present?
|
25
|
+
commit_mutexes.each do |mutex|
|
26
|
+
mutex.lock
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Group < ActiveRecord::Base
|
2
|
+
enable_locking :table_names => "users", :class_names => "User"
|
3
|
+
|
4
|
+
validates :name, :uniqueness => true, :presence => true
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
attr_accessor :validate_mutexes
|
11
|
+
attr_accessor :commit_mutexes
|
12
|
+
after_validation :release_mutexes
|
13
|
+
|
14
|
+
def release_mutexes
|
15
|
+
validate_mutexes.each do |mutex|
|
16
|
+
mutex.unlock
|
17
|
+
end
|
18
|
+
commit_mutexes.each do |mutex|
|
19
|
+
mutex.lock
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/spec/models/page.rb
ADDED
data/spec/models/user.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class User < ActiveRecord::Base
|
2
|
+
enable_locking :class_names => "Group"
|
3
|
+
|
4
|
+
validates :name, :uniqueness => true, :presence => true
|
5
|
+
|
6
|
+
attr_accessor :validate_mutexes
|
7
|
+
attr_accessor :commit_mutexes
|
8
|
+
after_validation :release_mutexes
|
9
|
+
|
10
|
+
def release_mutexes
|
11
|
+
validate_mutexes.each do |mutex|
|
12
|
+
mutex.unlock
|
13
|
+
end
|
14
|
+
commit_mutexes.each do |mutex|
|
15
|
+
mutex.lock
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'models/comment'
|
3
|
+
|
4
|
+
describe "Race conditions" do
|
5
|
+
it "should prevent race conditions on a single table" do
|
6
|
+
comment1 = Comment.new(title: "TITLE")
|
7
|
+
comment2 = Comment.new(title: "TITLE")
|
8
|
+
|
9
|
+
mutex1 = Mutex.new
|
10
|
+
mutex2 = Mutex.new
|
11
|
+
mutex3 = Mutex.new
|
12
|
+
mutex4 = Mutex.new
|
13
|
+
|
14
|
+
# This should not run within reasonable time
|
15
|
+
|
16
|
+
# main thread:
|
17
|
+
# 1. lock mutex1
|
18
|
+
# 2. lock mutex2
|
19
|
+
# 3. start threads
|
20
|
+
# 4. wait for mutex3 and mutex4 to be locked
|
21
|
+
# 4. release mutex1 and mutex2
|
22
|
+
|
23
|
+
# comment1:
|
24
|
+
# 0. lock mutex3
|
25
|
+
# 1. lock mutex1
|
26
|
+
# 2. run validations
|
27
|
+
# 3. release mutex1 and mutex3
|
28
|
+
# 4. lock mutex4
|
29
|
+
# 5. commit
|
30
|
+
# 6. release mutex4
|
31
|
+
|
32
|
+
# comment2:
|
33
|
+
# 1. lock mutex4
|
34
|
+
# 2. lock mutex2
|
35
|
+
# 2. run validations
|
36
|
+
# 3. release mutex2 and mutex4
|
37
|
+
# 4. lock mutex3
|
38
|
+
# 5. commit
|
39
|
+
# 6. release mutex3
|
40
|
+
|
41
|
+
mutex1.lock
|
42
|
+
mutex2.lock
|
43
|
+
|
44
|
+
comment1.validate_mutexes = [mutex1,mutex3]
|
45
|
+
comment1.commit_mutexes = [mutex4]
|
46
|
+
comment2.validate_mutexes = [mutex2,mutex4]
|
47
|
+
comment2.commit_mutexes = [mutex3]
|
48
|
+
|
49
|
+
# Start threads
|
50
|
+
thread1 = Thread.new do
|
51
|
+
mutex3.lock
|
52
|
+
mutex1.lock
|
53
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
54
|
+
comment1.save
|
55
|
+
end
|
56
|
+
mutex4.unlock
|
57
|
+
end
|
58
|
+
|
59
|
+
thread2 = Thread.new do
|
60
|
+
mutex4.lock
|
61
|
+
mutex2.lock
|
62
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
63
|
+
comment2.save
|
64
|
+
end
|
65
|
+
mutex3.unlock
|
66
|
+
end
|
67
|
+
|
68
|
+
until mutex3.locked?
|
69
|
+
sleep 0.001
|
70
|
+
end
|
71
|
+
until mutex4.locked?
|
72
|
+
sleep 0.001
|
73
|
+
end
|
74
|
+
|
75
|
+
mutex1.unlock
|
76
|
+
mutex2.unlock
|
77
|
+
thread1.join(3).should be_nil
|
78
|
+
thread2.join(3).should be_nil
|
79
|
+
|
80
|
+
comment1.persisted?.should == false
|
81
|
+
comment2.persisted?.should == false
|
82
|
+
|
83
|
+
# Clean up the threads
|
84
|
+
thread1.exit
|
85
|
+
thread2.exit
|
86
|
+
|
87
|
+
end
|
88
|
+
it "should prevent race conditions on validations spanning multiple tables" do
|
89
|
+
user = User.new(name: "root")
|
90
|
+
group = Group.new(name: "root")
|
91
|
+
|
92
|
+
mutex1 = Mutex.new
|
93
|
+
mutex2 = Mutex.new
|
94
|
+
mutex3 = Mutex.new
|
95
|
+
mutex4 = Mutex.new
|
96
|
+
|
97
|
+
# This should not run within reasonable time
|
98
|
+
|
99
|
+
# main thread:
|
100
|
+
# 1. lock mutex1
|
101
|
+
# 2. lock mutex2
|
102
|
+
# 3. start threads
|
103
|
+
# 4. wait for mutex3 and mutex4 to be locked
|
104
|
+
# 4. release mutex1 and mutex2
|
105
|
+
|
106
|
+
# user:
|
107
|
+
# 0. lock mutex3
|
108
|
+
# 1. lock mutex1
|
109
|
+
# 2. run validations
|
110
|
+
# 3. release mutex1 and mutex3
|
111
|
+
# 4. lock mutex4
|
112
|
+
# 5. commit
|
113
|
+
# 6. release mutex4
|
114
|
+
|
115
|
+
# group:
|
116
|
+
# 1. lock mutex4
|
117
|
+
# 2. lock mutex2
|
118
|
+
# 2. run validations
|
119
|
+
# 3. release mutex2 and mutex4
|
120
|
+
# 4. lock mutex3
|
121
|
+
# 5. commit
|
122
|
+
# 6. release mutex3
|
123
|
+
|
124
|
+
mutex1.lock
|
125
|
+
mutex2.lock
|
126
|
+
|
127
|
+
user.validate_mutexes = [mutex1,mutex3]
|
128
|
+
user.commit_mutexes = [mutex4]
|
129
|
+
group.validate_mutexes = [mutex2,mutex4]
|
130
|
+
group.commit_mutexes = [mutex3]
|
131
|
+
|
132
|
+
# Start threads
|
133
|
+
thread1 = Thread.new do
|
134
|
+
mutex3.lock
|
135
|
+
mutex1.lock
|
136
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
137
|
+
user.save
|
138
|
+
end
|
139
|
+
mutex4.unlock
|
140
|
+
end
|
141
|
+
|
142
|
+
thread2 = Thread.new do
|
143
|
+
mutex4.lock
|
144
|
+
mutex2.lock
|
145
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
146
|
+
group.save
|
147
|
+
end
|
148
|
+
mutex3.unlock
|
149
|
+
end
|
150
|
+
|
151
|
+
until mutex3.locked?
|
152
|
+
sleep 0.001
|
153
|
+
end
|
154
|
+
until mutex4.locked?
|
155
|
+
sleep 0.001
|
156
|
+
end
|
157
|
+
|
158
|
+
mutex1.unlock
|
159
|
+
mutex2.unlock
|
160
|
+
thread1.join(3).should be_nil
|
161
|
+
thread2.join(3).should be_nil
|
162
|
+
|
163
|
+
user.persisted?.should == false
|
164
|
+
group.persisted?.should == false
|
165
|
+
|
166
|
+
# Clean up the threads
|
167
|
+
thread1.exit
|
168
|
+
thread2.exit
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should pass a test which tries to create a huge amount of comments at roughly the same time with random times between validation and saving to the database" do
|
172
|
+
puts "This test will take a long time"
|
173
|
+
comments = []
|
174
|
+
threads = []
|
175
|
+
number_of_comments = 100
|
176
|
+
timeouts = AtomicInteger.new
|
177
|
+
# The wait times are sorted in reverse order to increase the chance of a race condition
|
178
|
+
wait_times = list_of_random_numbers(number_of_comments).sort.reverse
|
179
|
+
number_of_comments.times do
|
180
|
+
comment = Comment.new title: "RACE"
|
181
|
+
comment.wait_time = wait_times.shift
|
182
|
+
comments << comment
|
183
|
+
threads << Thread.new do
|
184
|
+
saved = false
|
185
|
+
until saved do
|
186
|
+
begin
|
187
|
+
ActiveRecord::Base.connection_pool.with_connection do |conn|
|
188
|
+
comment.save
|
189
|
+
saved = true
|
190
|
+
end
|
191
|
+
rescue ActiveRecord::ConnectionTimeoutError => e
|
192
|
+
# This test most likely triggers a lot of timeout errors
|
193
|
+
# We can safely ignore those errors, it's intended behaviour of this test
|
194
|
+
# To improve the accuracy of the tests, it's recommended to increase the poolsize
|
195
|
+
timeouts.increment
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
threads.each do |thread|
|
201
|
+
thread.join
|
202
|
+
end
|
203
|
+
comments.map(&:persisted?).select {|x| x == true}.size.should == 1
|
204
|
+
if timeouts.value > 0
|
205
|
+
puts "There were #{timeouts.value} timeouts during this test, increase your database connection pool size to improve accuracy"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'activerecord-tablelocks'
|
4
|
+
require 'rspec'
|
5
|
+
require 'rspec/autorun'
|
6
|
+
require 'active_record'
|
7
|
+
|
8
|
+
Dir[File.join(File.dirname(__FILE__),"support/**/*.rb")].each { |f| require f }
|
9
|
+
|
10
|
+
# For more progress output during the long test, uncomment the line below
|
11
|
+
# ActiveRecord::Base.logger = Logger.new(STDOUT)
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
config = case ENV["ENGINE"]
|
2
|
+
when "postgres"
|
3
|
+
{
|
4
|
+
"adapter" => "postgresql",
|
5
|
+
"database" => ENV["DATABASE"],
|
6
|
+
"username" => ENV["USERNAME"],
|
7
|
+
"password" => ENV["PASSWORD"],
|
8
|
+
"pool" => "15"
|
9
|
+
}
|
10
|
+
else
|
11
|
+
raise "Only Postgres is supported at this time"
|
12
|
+
end
|
13
|
+
|
14
|
+
ActiveRecord::Migration.verbose = false
|
15
|
+
ActiveRecord::Base.configurations = { "test" => config }
|
16
|
+
ActiveRecord::Base.establish_connection("test")
|
17
|
+
ActiveRecord::Base.default_timezone = :utc
|
18
|
+
|
19
|
+
|
20
|
+
ActiveRecord::Schema.define do
|
21
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
22
|
+
drop_table table
|
23
|
+
end
|
24
|
+
create_table :pages do |t|
|
25
|
+
t.string :name
|
26
|
+
t.text :content
|
27
|
+
end
|
28
|
+
create_table :comments do |t|
|
29
|
+
t.string :title
|
30
|
+
t.text :content
|
31
|
+
end
|
32
|
+
create_table :users do |t|
|
33
|
+
t.string :name
|
34
|
+
end
|
35
|
+
create_table :groups do |t|
|
36
|
+
t.string :name
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-tablelocks
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sernin van de Krol
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '1.2'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '1.2'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: railties
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 2.14.1
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.14.1
|
94
|
+
description: This gem enables the use of database specific table locks when saving
|
95
|
+
or destroying your ActiveRecord objects. This ensures no race conditions exist when
|
96
|
+
using e.g. validates_uniqueness_of.
|
97
|
+
email:
|
98
|
+
- serninpc@paneidos.net
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- .gitignore
|
104
|
+
- .rspec
|
105
|
+
- Gemfile
|
106
|
+
- LICENSE.txt
|
107
|
+
- README.md
|
108
|
+
- Rakefile
|
109
|
+
- activerecord-tablelocks.gemspec
|
110
|
+
- lib/activerecord-tablelocks.rb
|
111
|
+
- lib/activerecord/tablelocks/activerecord.rb
|
112
|
+
- lib/activerecord/tablelocks/activerecord/postgres.rb
|
113
|
+
- lib/activerecord/tablelocks/railtie.rb
|
114
|
+
- lib/activerecord/tablelocks/version.rb
|
115
|
+
- spec/extension_methods_spec.rb
|
116
|
+
- spec/locking_targets_spec.rb
|
117
|
+
- spec/models/comment.rb
|
118
|
+
- spec/models/group.rb
|
119
|
+
- spec/models/page.rb
|
120
|
+
- spec/models/user.rb
|
121
|
+
- spec/normal_behaviour_spec.rb
|
122
|
+
- spec/race_conditions_spec.rb
|
123
|
+
- spec/spec_helper.rb
|
124
|
+
- spec/support/atomic_integer.rb
|
125
|
+
- spec/support/database.rb
|
126
|
+
- spec/support/random.rb
|
127
|
+
homepage: https://github.com/paneidos/activerecord-tablelocks
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ! '>='
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
segments:
|
141
|
+
- 0
|
142
|
+
hash: 3007498809683035239
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
none: false
|
145
|
+
requirements:
|
146
|
+
- - ! '>='
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
segments:
|
150
|
+
- 0
|
151
|
+
hash: 3007498809683035239
|
152
|
+
requirements: []
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 1.8.25
|
155
|
+
signing_key:
|
156
|
+
specification_version: 3
|
157
|
+
summary: Use native table locks of your database for your ActiveRecord models
|
158
|
+
test_files:
|
159
|
+
- spec/extension_methods_spec.rb
|
160
|
+
- spec/locking_targets_spec.rb
|
161
|
+
- spec/models/comment.rb
|
162
|
+
- spec/models/group.rb
|
163
|
+
- spec/models/page.rb
|
164
|
+
- spec/models/user.rb
|
165
|
+
- spec/normal_behaviour_spec.rb
|
166
|
+
- spec/race_conditions_spec.rb
|
167
|
+
- spec/spec_helper.rb
|
168
|
+
- spec/support/atomic_integer.rb
|
169
|
+
- spec/support/database.rb
|
170
|
+
- spec/support/random.rb
|