aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test
diff options
context:
space:
mode:
authorJeremy Kemper <jeremy@bitsweat.net>2006-06-19 22:48:51 +0000
committerJeremy Kemper <jeremy@bitsweat.net>2006-06-19 22:48:51 +0000
commit15aa6e05528019fbf62e1a5cbd2398a2205af8bb (patch)
tree6cdedee3b5f3ef0a075e55c85e3bc4607c43a3e0 /activerecord/test
parente5fc5aaffe0d97b73678d7edfcaca222f01aba69 (diff)
downloadrails-15aa6e05528019fbf62e1a5cbd2398a2205af8bb.tar.gz
rails-15aa6e05528019fbf62e1a5cbd2398a2205af8bb.tar.bz2
rails-15aa6e05528019fbf62e1a5cbd2398a2205af8bb.zip
r4644@asus: jeremy | 2006-06-16 14:57:03 -0700
locking r4645@asus: jeremy | 2006-06-17 12:41:30 -0700 missing reply fixture r4646@asus: jeremy | 2006-06-19 13:05:23 -0700 Use a per-thread (rather than global) transaction mutex so you may execute concurrent transactions on separate connections. r4647@asus: jeremy | 2006-06-19 13:07:23 -0700 PostgreSQL: introduce allow_concurrency option which determines whether to use blocking or asynchronous #execute. Adapters with blocking #execute will deadlock Ruby threads. The default value is ActiveRecord::Base.allow_concurrency. r4648@asus: jeremy | 2006-06-19 13:08:40 -0700 Pass the default allow_concurrency when instantiating new connections. r4649@asus: jeremy | 2006-06-19 13:11:12 -0700 Break out concurrent transaction tests and run them for PostgreSQLAdapter only (need to fork or system('some_test_script') for the other adapters) r4650@asus: jeremy | 2006-06-19 13:42:48 -0700 Row locking. Provide a locking clause with the :lock finder option or true for the default "FOR UPDATE". r4661@asus: jeremy | 2006-06-19 15:36:51 -0700 excise the junk mutex git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4460 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord/test')
-rw-r--r--activerecord/test/locking_test.rb91
-rw-r--r--activerecord/test/threaded_connections_test.rb1
-rw-r--r--activerecord/test/transactions_test.rb143
3 files changed, 163 insertions, 72 deletions
diff --git a/activerecord/test/locking_test.rb b/activerecord/test/locking_test.rb
index 105f19f2bc..bacc7b8ae0 100644
--- a/activerecord/test/locking_test.rb
+++ b/activerecord/test/locking_test.rb
@@ -2,16 +2,16 @@ require 'abstract_unit'
require 'fixtures/person'
require 'fixtures/legacy_thing'
-class LockingTest < Test::Unit::TestCase
+class OptimisticLockingTest < Test::Unit::TestCase
fixtures :people, :legacy_things
def test_lock_existing
p1 = Person.find(1)
p2 = Person.find(1)
-
+
p1.first_name = "Michael"
p1.save
-
+
assert_raises(ActiveRecord::StaleObjectError) {
p2.first_name = "should fail"
p2.save
@@ -24,13 +24,13 @@ class LockingTest < Test::Unit::TestCase
assert_equal p1.id, p2.id
p1.first_name = "Anika"
p1.save
-
+
assert_raises(ActiveRecord::StaleObjectError) {
p2.first_name = "should fail"
p2.save
}
end
-
+
def test_lock_column_name_existing
t1 = LegacyThing.find(1)
t2 = LegacyThing.find(1)
@@ -41,6 +41,85 @@ class LockingTest < Test::Unit::TestCase
t2.tps_report_number = 300
t2.save
}
- end
+ end
+end
+
+
+# TODO: test against the generated SQL since testing locking behavior itself
+# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
+# blocks, so separate script called by Kernel#system is needed.
+# (See exec vs. async_exec in the PostgreSQL adapter.)
+class PessimisticLockingTest < Test::Unit::TestCase
+ self.use_transactional_fixtures = false
+ fixtures :people
+
+ def setup
+ @allow_concurrency = ActiveRecord::Base.allow_concurrency
+ ActiveRecord::Base.allow_concurrency = true
+ end
+
+ def teardown
+ ActiveRecord::Base.allow_concurrency = @allow_concurrency
+ end
+ # Test that the adapter doesn't blow up on add_lock!
+ def test_sane_find_with_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.find 1, :lock => true
+ end
+ end
+ end
+
+ # Test no-blowup for scoped lock.
+ def test_sane_find_with_lock
+ assert_nothing_raised do
+ Person.transaction do
+ Person.with_scope(:find => { :lock => true }) do
+ Person.find 1
+ end
+ end
+ end
+ end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_no_locks_no_wait
+ first, second = duel { Person.find 1 }
+ assert first.end > second.end
+ end
+
+ def test_second_lock_waits
+ first, second = duel { Person.find 1, :lock => true }
+ assert second.end > first.end
+ end
+
+ protected
+ def duel(zzz = 0.2)
+ t0, t1, t2, t3 = nil, nil, nil, nil
+
+ a = Thread.new do
+ t0 = Time.now
+ Person.transaction do
+ yield
+ sleep zzz # block thread 2 for zzz seconds
+ end
+ t1 = Time.now
+ end
+
+ b = Thread.new do
+ sleep zzz / 2.0 # ensure thread 1 tx starts first
+ t2 = Time.now
+ Person.transaction { yield }
+ t3 = Time.now
+ end
+
+ a.join
+ b.join
+
+ assert t1 > t0 + zzz
+ assert t2 > t0
+ assert t3 > t2
+ [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
+ end
+ end
end
diff --git a/activerecord/test/threaded_connections_test.rb b/activerecord/test/threaded_connections_test.rb
index a812ec642c..aaa56b3bfe 100644
--- a/activerecord/test/threaded_connections_test.rb
+++ b/activerecord/test/threaded_connections_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'fixtures/topic'
+require 'fixtures/reply'
unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name
class ThreadedConnectionsTest < Test::Unit::TestCase
diff --git a/activerecord/test/transactions_test.rb b/activerecord/test/transactions_test.rb
index 421da4d7a6..a8584cfd56 100644
--- a/activerecord/test/transactions_test.rb
+++ b/activerecord/test/transactions_test.rb
@@ -5,12 +5,9 @@ require 'fixtures/developer'
class TransactionTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
-
fixtures :topics, :developers
def setup
- # sqlite does not seem to return these in the right order, so we sort them
- # explicitly for sqlite's sake. sqlite3 does fine.
@first, @second = Topic.find(1, 2).sort_by { |t| t.id }
end
@@ -137,81 +134,95 @@ class TransactionTest < Test::Unit::TestCase
assert !Topic.find(2).approved?, "Second should have been unapproved"
end
- # This will cause transactions to overlap and fail unless they are
- # performed on separate database connections.
- def test_transaction_per_thread
- assert_nothing_raised do
- threads = (1..20).map do
- Thread.new do
- Topic.transaction do
- topic = Topic.find(:first)
- topic.approved = !topic.approved?
- topic.save!
- topic.approved = !topic.approved?
- topic.save!
+ private
+ def add_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { def after_save() raise "Make the transaction rollback" end }
+ end
+
+ def remove_exception_raising_after_save_callback_to_topic
+ Topic.class_eval { remove_method :after_save }
+ end
+end
+
+if current_adapter?(:PostgreSQLAdapter)
+ class ConcurrentTransactionTest < TransactionTest
+ def setup
+ @allow_concurrency = ActiveRecord::Base.allow_concurrency
+ ActiveRecord::Base.allow_concurrency = true
+ super
+ end
+
+ def teardown
+ super
+ ActiveRecord::Base.allow_concurrency = @allow_concurrency
+ end
+
+ # This will cause transactions to overlap and fail unless they are performed on
+ # separate database connections.
+ def test_transaction_per_thread
+ assert_nothing_raised do
+ threads = (1..3).map do
+ Thread.new do
+ Topic.transaction do
+ topic = Topic.find(1)
+ topic.approved = !topic.approved?
+ topic.save!
+ topic.approved = !topic.approved?
+ topic.save!
+ end
end
end
- end
- threads.each { |t| t.join }
+ threads.each { |t| t.join }
+ end
end
- end
- # Test for dirty reads among simultaneous transactions.
- def test_transaction_isolation__read_committed
- # Should be invariant.
- original_salary = Developer.find(1).salary
- temporary_salary = 200000
-
- assert_nothing_raised do
- threads = (1..20).map do
- Thread.new do
- Developer.transaction do
- # Expect original salary.
- dev = Developer.find(1)
- assert_equal original_salary, dev.salary
-
- dev.salary = temporary_salary
- dev.save!
-
- # Expect temporary salary.
- dev = Developer.find(1)
- assert_equal temporary_salary, dev.salary
-
- dev.salary = original_salary
- dev.save!
-
- # Expect original salary.
- dev = Developer.find(1)
- assert_equal original_salary, dev.salary
+ # Test for dirty reads among simultaneous transactions.
+ def test_transaction_isolation__read_committed
+ # Should be invariant.
+ original_salary = Developer.find(1).salary
+ temporary_salary = 200000
+
+ assert_nothing_raised do
+ threads = (1..3).map do
+ Thread.new do
+ Developer.transaction do
+ # Expect original salary.
+ dev = Developer.find(1)
+ assert_equal original_salary, dev.salary
+
+ dev.salary = temporary_salary
+ dev.save!
+
+ # Expect temporary salary.
+ dev = Developer.find(1)
+ assert_equal temporary_salary, dev.salary
+
+ dev.salary = original_salary
+ dev.save!
+
+ # Expect original salary.
+ dev = Developer.find(1)
+ assert_equal original_salary, dev.salary
+ end
end
end
- end
- # Keep our eyes peeled.
- threads << Thread.new do
- 10.times do
- sleep 0.05
- Developer.transaction do
- # Always expect original salary.
- assert_equal original_salary, Developer.find(1).salary
+ # Keep our eyes peeled.
+ threads << Thread.new do
+ 10.times do
+ sleep 0.05
+ Developer.transaction do
+ # Always expect original salary.
+ assert_equal original_salary, Developer.find(1).salary
+ end
end
end
+
+ threads.each { |t| t.join }
end
- threads.each { |t| t.join }
+ assert_equal original_salary, Developer.find(1).salary
end
-
- assert_equal original_salary, Developer.find(1).salary
end
-
-
- private
- def add_exception_raising_after_save_callback_to_topic
- Topic.class_eval { def after_save() raise "Make the transaction rollback" end }
- end
-
- def remove_exception_raising_after_save_callback_to_topic
- Topic.class_eval { remove_method :after_save }
- end
end