2008-01-21 12:20:51 -05:00
|
|
|
require "cases/helper"
|
2008-01-18 02:31:37 -05:00
|
|
|
require 'models/person'
|
|
|
|
require 'models/reader'
|
|
|
|
require 'models/legacy_thing'
|
2008-04-30 16:04:08 -04:00
|
|
|
require 'models/reference'
|
2004-12-31 14:38:04 -05:00
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
class LockWithoutDefault < ActiveRecord::Base; end
|
|
|
|
|
|
|
|
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
|
2006-11-01 15:28:48 -05:00
|
|
|
set_table_name :lock_without_defaults_cust
|
2006-09-03 20:02:38 -04:00
|
|
|
set_locking_column :custom_lock_version
|
|
|
|
end
|
|
|
|
|
2007-11-16 15:31:24 -05:00
|
|
|
class ReadonlyFirstNamePerson < Person
|
|
|
|
attr_readonly :first_name
|
|
|
|
end
|
|
|
|
|
2008-01-21 12:20:51 -05:00
|
|
|
class OptimisticLockingTest < ActiveRecord::TestCase
|
2008-04-30 16:04:08 -04:00
|
|
|
fixtures :people, :legacy_things, :references
|
2004-12-31 14:38:04 -05:00
|
|
|
|
2007-02-07 11:10:40 -05:00
|
|
|
# need to disable transactional fixtures, because otherwise the sqlite3
|
|
|
|
# adapter (at least) chokes when we try and change the schema in the middle
|
|
|
|
# of a test (see test_increment_counter_*).
|
|
|
|
self.use_transactional_fixtures = false
|
|
|
|
|
2004-12-31 14:38:04 -05:00
|
|
|
def test_lock_existing
|
|
|
|
p1 = Person.find(1)
|
|
|
|
p2 = Person.find(1)
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
2006-06-19 18:48:51 -04:00
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p1.first_name = 'stu'
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
2006-06-19 18:48:51 -04:00
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p2.first_name = 'sue'
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
|
2004-12-31 14:38:04 -05:00
|
|
|
end
|
2008-01-18 02:30:42 -05:00
|
|
|
|
2007-05-25 17:31:44 -04:00
|
|
|
def test_lock_repeating
|
|
|
|
p1 = Person.find(1)
|
|
|
|
p2 = Person.find(1)
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p1.first_name = 'stu'
|
2007-05-25 17:31:44 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p2.first_name = 'sue'
|
2007-05-25 17:31:44 -04:00
|
|
|
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
|
2008-06-15 16:25:59 -04:00
|
|
|
p2.first_name = 'sue2'
|
2007-05-25 17:31:44 -04:00
|
|
|
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
|
|
|
|
end
|
2004-12-31 14:38:04 -05:00
|
|
|
|
|
|
|
def test_lock_new
|
2006-06-20 15:54:35 -04:00
|
|
|
p1 = Person.new(:first_name => 'anika')
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p1.first_name = 'anika2'
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
2004-12-31 14:38:04 -05:00
|
|
|
p2 = Person.find(p1.id)
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p1.first_name = 'anika3'
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal 0, p2.lock_version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p2.first_name = 'sue'
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
|
2006-01-14 04:36:52 -05:00
|
|
|
end
|
2008-01-18 02:30:42 -05:00
|
|
|
|
2007-12-14 20:45:30 -05:00
|
|
|
def test_lock_new_with_nil
|
|
|
|
p1 = Person.new(:first_name => 'anika')
|
|
|
|
p1.save!
|
|
|
|
p1.lock_version = nil # simulate bad fixture or column with no default
|
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
end
|
2008-01-18 02:30:42 -05:00
|
|
|
|
2006-06-19 18:48:51 -04:00
|
|
|
|
2006-01-14 04:36:52 -05:00
|
|
|
def test_lock_column_name_existing
|
|
|
|
t1 = LegacyThing.find(1)
|
|
|
|
t2 = LegacyThing.find(1)
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_equal 0, t1.version
|
|
|
|
assert_equal 0, t2.version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
t1.tps_report_number = 700
|
2006-06-20 15:54:35 -04:00
|
|
|
t1.save!
|
|
|
|
assert_equal 1, t1.version
|
|
|
|
assert_equal 0, t2.version
|
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
t2.tps_report_number = 800
|
2006-06-20 15:54:35 -04:00
|
|
|
assert_raises(ActiveRecord::StaleObjectError) { t2.save! }
|
|
|
|
end
|
|
|
|
|
|
|
|
def test_lock_column_is_mass_assignable
|
|
|
|
p1 = Person.create(:first_name => 'bianca')
|
|
|
|
assert_equal 0, p1.lock_version
|
|
|
|
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
|
2006-01-14 04:36:52 -05:00
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
p1.first_name = 'bianca2'
|
2006-06-20 15:54:35 -04:00
|
|
|
p1.save!
|
|
|
|
assert_equal 1, p1.lock_version
|
|
|
|
assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
def test_lock_without_default_sets_version_to_zero
|
|
|
|
t1 = LockWithoutDefault.new
|
|
|
|
assert_equal 0, t1.lock_version
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-09-03 20:02:38 -04:00
|
|
|
def test_lock_with_custom_column_without_default_sets_version_to_zero
|
|
|
|
t1 = LockWithCustomColumnWithoutDefault.new
|
|
|
|
assert_equal 0, t1.custom_lock_version
|
|
|
|
end
|
2007-02-07 11:10:40 -05:00
|
|
|
|
2007-11-16 15:31:24 -05:00
|
|
|
def test_readonly_attributes
|
2007-11-28 15:12:49 -05:00
|
|
|
assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes
|
2007-11-16 15:31:24 -05:00
|
|
|
|
|
|
|
p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name")
|
|
|
|
p.reload
|
|
|
|
assert_equal "unchangeable name", p.first_name
|
|
|
|
|
|
|
|
p.update_attributes(:first_name => "changed name")
|
|
|
|
p.reload
|
|
|
|
assert_equal "unchangeable name", p.first_name
|
|
|
|
end
|
|
|
|
|
2007-02-07 11:10:40 -05:00
|
|
|
{ :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
|
|
|
|
define_method("test_increment_counter_updates_#{name}") do
|
|
|
|
counter_test model, 1 do |id|
|
|
|
|
model.increment_counter :test_count, id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method("test_decrement_counter_updates_#{name}") do
|
|
|
|
counter_test model, -1 do |id|
|
|
|
|
model.decrement_counter :test_count, id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
define_method("test_update_counters_updates_#{name}") do
|
|
|
|
counter_test model, 1 do |id|
|
|
|
|
model.update_counters id, :test_count => 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2008-04-30 16:04:08 -04:00
|
|
|
|
|
|
|
def test_quote_table_name
|
|
|
|
ref = references(:michael_magician)
|
|
|
|
ref.favourite = !ref.favourite
|
|
|
|
assert ref.save
|
|
|
|
end
|
2007-02-07 11:10:40 -05:00
|
|
|
|
2008-06-15 16:25:59 -04:00
|
|
|
# Useful for partial updates, don't only update the lock_version if there
|
|
|
|
# is nothing else being updated.
|
|
|
|
def test_update_without_attributes_does_not_only_update_lock_version
|
|
|
|
assert_nothing_raised do
|
|
|
|
p1 = Person.new(:first_name => 'anika')
|
|
|
|
p1.send(:update_with_lock, [])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2007-02-07 11:10:40 -05:00
|
|
|
private
|
|
|
|
|
|
|
|
def add_counter_column_to(model)
|
|
|
|
model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
|
|
|
|
model.reset_column_information
|
2007-09-13 19:21:14 -04:00
|
|
|
# OpenBase does not set a value to existing rows when adding a not null default column
|
|
|
|
model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
|
2007-02-07 11:10:40 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def remove_counter_column_from(model)
|
|
|
|
model.connection.remove_column model.table_name, :test_count
|
|
|
|
model.reset_column_information
|
|
|
|
end
|
|
|
|
|
|
|
|
def counter_test(model, expected_count)
|
|
|
|
add_counter_column_to(model)
|
|
|
|
object = model.find(:first)
|
|
|
|
assert_equal 0, object.test_count
|
|
|
|
assert_equal 0, object.send(model.locking_column)
|
|
|
|
yield object.id
|
|
|
|
object.reload
|
|
|
|
assert_equal expected_count, object.test_count
|
|
|
|
assert_equal 1, object.send(model.locking_column)
|
|
|
|
ensure
|
|
|
|
remove_counter_column_from(model)
|
|
|
|
end
|
2006-06-19 18:48:51 -04:00
|
|
|
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.)
|
|
|
|
|
2007-09-13 19:21:14 -04:00
|
|
|
# TODO: The SQL Server, Sybase, and OpenBase adapters currently have no support for pessimistic locking
|
2006-01-14 04:36:52 -05:00
|
|
|
|
2007-09-13 19:21:14 -04:00
|
|
|
unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter)
|
2008-01-21 12:20:51 -05:00
|
|
|
class PessimisticLockingTest < ActiveRecord::TestCase
|
2006-07-10 14:24:35 -04:00
|
|
|
self.use_transactional_fixtures = false
|
2006-12-06 15:28:26 -05:00
|
|
|
fixtures :people, :readers
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
def setup
|
2006-12-06 15:28:26 -05:00
|
|
|
# Avoid introspection queries during tests.
|
|
|
|
Person.columns; Reader.columns
|
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
@allow_concurrency = ActiveRecord::Base.allow_concurrency
|
|
|
|
ActiveRecord::Base.allow_concurrency = true
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
def teardown
|
|
|
|
ActiveRecord::Base.allow_concurrency = @allow_concurrency
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
|
|
|
# Test typical find.
|
2006-07-10 14:24:35 -04:00
|
|
|
def test_sane_find_with_lock
|
|
|
|
assert_nothing_raised do
|
|
|
|
Person.transaction do
|
|
|
|
Person.find 1, :lock => true
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
|
|
|
# Test scoped lock.
|
|
|
|
def test_sane_find_with_scoped_lock
|
2006-07-10 14:24:35 -04:00
|
|
|
assert_nothing_raised do
|
|
|
|
Person.transaction do
|
|
|
|
Person.with_scope(:find => { :lock => true }) do
|
|
|
|
Person.find 1
|
|
|
|
end
|
|
|
|
end
|
2006-06-19 21:58:36 -04:00
|
|
|
end
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
|
|
|
# PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
|
|
|
|
unless current_adapter?(:PostgreSQLAdapter)
|
|
|
|
# Test locked eager find.
|
|
|
|
def test_eager_find_with_lock
|
|
|
|
assert_nothing_raised do
|
|
|
|
Person.transaction do
|
2006-12-06 15:28:26 -05:00
|
|
|
Person.find 1, :include => :readers, :lock => true
|
2006-12-05 17:07:55 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
# Locking a record reloads it.
|
|
|
|
def test_sane_lock_method
|
|
|
|
assert_nothing_raised do
|
|
|
|
Person.transaction do
|
|
|
|
person = Person.find 1
|
|
|
|
old, person.first_name = person.first_name, 'fooman'
|
|
|
|
person.lock!
|
|
|
|
assert_equal old, person.first_name
|
|
|
|
end
|
|
|
|
end
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
|
|
|
|
def test_no_locks_no_wait
|
|
|
|
first, second = duel { Person.find 1 }
|
|
|
|
assert first.end > second.end
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
def test_second_lock_waits
|
|
|
|
assert [0.2, 1, 5].any? { |zzz|
|
|
|
|
first, second = duel(zzz) { Person.find 1, :lock => true }
|
|
|
|
second.end > first.end
|
|
|
|
}
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
protected
|
|
|
|
def duel(zzz = 5)
|
|
|
|
t0, t1, t2, t3 = nil, nil, nil, nil
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
a = Thread.new do
|
|
|
|
t0 = Time.now
|
|
|
|
Person.transaction do
|
|
|
|
yield
|
|
|
|
sleep zzz # block thread 2 for zzz seconds
|
|
|
|
end
|
|
|
|
t1 = Time.now
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
b = Thread.new do
|
|
|
|
sleep zzz / 2.0 # ensure thread 1 tx starts first
|
|
|
|
t2 = Time.now
|
|
|
|
Person.transaction { yield }
|
|
|
|
t3 = Time.now
|
|
|
|
end
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
a.join
|
|
|
|
b.join
|
2006-12-05 17:07:55 -05:00
|
|
|
|
2006-07-10 14:24:35 -04:00
|
|
|
assert t1 > t0 + zzz
|
|
|
|
assert t2 > t0
|
|
|
|
assert t3 > t2
|
|
|
|
[t0.to_f..t1.to_f, t2.to_f..t3.to_f]
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2006-07-10 14:24:35 -04:00
|
|
|
end
|
2006-06-19 18:48:51 -04:00
|
|
|
end
|
2005-06-10 10:58:02 -04:00
|
|
|
end
|