From 15aa6e05528019fbf62e1a5cbd2398a2205af8bb Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Mon, 19 Jun 2006 22:48:51 +0000 Subject: 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 --- activerecord/test/locking_test.rb | 91 ++++++++++++++-- activerecord/test/threaded_connections_test.rb | 1 + activerecord/test/transactions_test.rb | 143 +++++++++++++------------ 3 files changed, 163 insertions(+), 72 deletions(-) (limited to 'activerecord/test') 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 -- cgit v1.2.3