mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Move transaction thread-safety test to transactions_test. Check that simultaneous transactions don't step on each others' toes. Check that simultaneous transactions don't give dirty reads (read-committed txn isolation or greater.)
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2417 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
4ce9b4c557
commit
e828657973
2 changed files with 79 additions and 47 deletions
|
@ -1,36 +0,0 @@
|
|||
require 'abstract_unit'
|
||||
require 'fixtures/topic'
|
||||
|
||||
class ThreadSafetyTest < Test::Unit::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
fixtures :topics
|
||||
|
||||
def setup
|
||||
@threads = []
|
||||
end
|
||||
|
||||
def test_threading_on_transactions
|
||||
# SQLite breaks down under thread banging
|
||||
# Jamis Buck (author of SQLite-ruby): "I know that sqlite itself is not designed for concurrent access"
|
||||
if ActiveRecord::ConnectionAdapters.const_defined? :SQLiteAdapter
|
||||
return true if ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::SQLiteAdapter)
|
||||
end
|
||||
|
||||
5.times do |thread_number|
|
||||
@threads << Thread.new(thread_number) do |thread_number|
|
||||
first, second = Topic.find(1, 2)
|
||||
Topic.transaction(first, second) do
|
||||
Topic.logger.info "started #{thread_number}"
|
||||
first.approved = 1
|
||||
second.approved = 0
|
||||
first.save
|
||||
second.save
|
||||
Topic.logger.info "ended #{thread_number}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@threads.each { |t| t.join }
|
||||
end
|
||||
end
|
|
@ -1,11 +1,11 @@
|
|||
require 'abstract_unit'
|
||||
require 'fixtures/topic'
|
||||
|
||||
require 'fixtures/developer'
|
||||
|
||||
class TransactionTest < Test::Unit::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
fixtures :topics
|
||||
fixtures :topics, :developers
|
||||
|
||||
def setup
|
||||
# sqlite does not seem to return these in the right order, so we sort them
|
||||
|
@ -15,8 +15,8 @@ class TransactionTest < Test::Unit::TestCase
|
|||
|
||||
def test_successful
|
||||
Topic.transaction do
|
||||
@first.approved = 1
|
||||
@second.approved = 0
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save
|
||||
@second.save
|
||||
end
|
||||
|
@ -27,8 +27,8 @@ class TransactionTest < Test::Unit::TestCase
|
|||
|
||||
def transaction_with_return
|
||||
Topic.transaction do
|
||||
@first.approved = 1
|
||||
@second.approved = 0
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save
|
||||
@second.save
|
||||
return
|
||||
|
@ -58,8 +58,8 @@ class TransactionTest < Test::Unit::TestCase
|
|||
|
||||
def test_successful_with_instance_method
|
||||
@first.transaction do
|
||||
@first.approved = 1
|
||||
@second.approved = 0
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save
|
||||
@second.save
|
||||
end
|
||||
|
@ -125,8 +125,8 @@ class TransactionTest < Test::Unit::TestCase
|
|||
def test_nested_explicit_transactions
|
||||
Topic.transaction do
|
||||
Topic.transaction do
|
||||
@first.approved = 1
|
||||
@second.approved = 0
|
||||
@first.approved = true
|
||||
@second.approved = false
|
||||
@first.save
|
||||
@second.save
|
||||
end
|
||||
|
@ -135,7 +135,75 @@ class TransactionTest < Test::Unit::TestCase
|
|||
assert Topic.find(1).approved?, "First should have been approved"
|
||||
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!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
threads.each { |t| t.join }
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
threads.each { |t| t.join }
|
||||
end
|
||||
|
||||
assert_equal original_salary, Developer.find(1).salary
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def add_exception_raising_after_save_callback_to_topic
|
||||
|
|
Loading…
Reference in a new issue