diff options
Diffstat (limited to 'activerecord/test/cases/locking_test.rb')
-rw-r--r-- | activerecord/test/cases/locking_test.rb | 409 |
1 files changed, 306 insertions, 103 deletions
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 9e4998a946..3701be4b11 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -1,24 +1,26 @@ -require 'thread' +# frozen_string_literal: true + +require "thread" require "cases/helper" -require 'models/person' -require 'models/job' -require 'models/reader' -require 'models/ship' -require 'models/legacy_thing' -require 'models/personal_legacy_thing' -require 'models/reference' -require 'models/string_key_object' -require 'models/car' -require 'models/bulb' -require 'models/engine' -require 'models/wheel' -require 'models/treasure' +require "models/person" +require "models/job" +require "models/reader" +require "models/ship" +require "models/legacy_thing" +require "models/personal_legacy_thing" +require "models/reference" +require "models/string_key_object" +require "models/car" +require "models/bulb" +require "models/engine" +require "models/wheel" +require "models/treasure" class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.table_name = :lock_without_defaults_cust - self.column_defaults # to test @column_defaults caching. + column_defaults # to test @column_defaults caching. self.locking_column = :custom_lock_version end @@ -33,7 +35,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1 = Person.find(1) assert_equal 0, p1.lock_version - p1.first_name = 'anika2' + p1.first_name = "anika2" p1.save! assert_equal 1, p1.lock_version @@ -45,12 +47,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, s1.lock_version assert_equal 0, s2.lock_version - s1.name = 'updated record' + s1.name = "updated record" s1.save! assert_equal 1, s1.lock_version assert_equal 0, s2.lock_version - s2.name = 'doubly updated record' + s2.name = "doubly updated record" assert_raise(ActiveRecord::StaleObjectError) { s2.save! } end @@ -60,7 +62,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, s1.lock_version assert_equal 0, s2.lock_version - s1.name = 'updated record' + s1.name = "updated record" s1.save! assert_equal 1, s1.lock_version assert_equal 0, s2.lock_version @@ -78,12 +80,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version - p1.first_name = 'stu' + p1.first_name = "stu" p1.save! assert_equal 1, p1.lock_version assert_equal 0, p2.lock_version - p2.first_name = 'sue' + p2.first_name = "sue" assert_raise(ActiveRecord::StaleObjectError) { p2.save! } end @@ -94,7 +96,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version - p1.first_name = 'stu' + p1.first_name = "stu" p1.save! assert_equal 1, p1.lock_version assert_equal 0, p2.lock_version @@ -113,60 +115,64 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version - p1.first_name = 'stu' + p1.first_name = "stu" p1.save! assert_equal 1, p1.lock_version assert_equal 0, p2.lock_version - p2.first_name = 'sue' + p2.first_name = "sue" assert_raise(ActiveRecord::StaleObjectError) { p2.save! } - p2.first_name = 'sue2' + p2.first_name = "sue2" assert_raise(ActiveRecord::StaleObjectError) { p2.save! } end def test_lock_new - p1 = Person.new(:first_name => 'anika') + p1 = Person.new(first_name: "anika") assert_equal 0, p1.lock_version - p1.first_name = 'anika2' + p1.first_name = "anika2" p1.save! p2 = Person.find(p1.id) assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version - p1.first_name = 'anika3' + p1.first_name = "anika3" p1.save! assert_equal 1, p1.lock_version assert_equal 0, p2.lock_version - p2.first_name = 'sue' + p2.first_name = "sue" assert_raise(ActiveRecord::StaleObjectError) { p2.save! } end def test_lock_exception_record - p1 = Person.new(:first_name => 'mira') + p1 = Person.new(first_name: "mira") assert_equal 0, p1.lock_version - p1.first_name = 'mira2' + p1.first_name = "mira2" p1.save! p2 = Person.find(p1.id) assert_equal 0, p1.lock_version assert_equal 0, p2.lock_version - p1.first_name = 'mira3' + p1.first_name = "mira3" p1.save! - p2.first_name = 'sue' + p2.first_name = "sue" error = assert_raise(ActiveRecord::StaleObjectError) { p2.save! } assert_equal(error.record.object_id, p2.object_id) end - def test_lock_new_with_nil - p1 = Person.new(:first_name => 'anika') + def test_lock_new_when_explicitly_passing_nil + p1 = Person.new(first_name: "anika", lock_version: nil) p1.save! - p1.lock_version = nil # simulate bad fixture or column with no default + assert_equal 0, p1.lock_version + end + + def test_lock_new_when_explicitly_passing_value + p1 = Person.new(first_name: "Douglas Adams", lock_version: 42) p1.save! - assert_equal 1, p1.lock_version + assert_equal 42, p1.lock_version end def test_touch_existing_lock @@ -175,6 +181,30 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1.touch assert_equal 1, p1.lock_version + assert_not p1.changed?, "Changes should have been cleared" + end + + def test_touch_stale_object + person = Person.create!(first_name: "Mehmet Emin") + stale_person = Person.find(person.id) + person.update_attribute(:gender, "M") + + assert_raises(ActiveRecord::StaleObjectError) do + stale_person.touch + end + end + + def test_explicit_update_lock_column_raise_error + person = Person.find(1) + + assert_raises(ActiveRecord::StaleObjectError) do + person.first_name = "Douglas Adams" + person.lock_version = 42 + + assert person.lock_version_changed? + + person.save + end end def test_lock_column_name_existing @@ -193,11 +223,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase end def test_lock_column_is_mass_assignable - p1 = Person.create(:first_name => 'bianca') + p1 = Person.create(first_name: "bianca") assert_equal 0, p1.lock_version assert_equal p1.lock_version, Person.new(p1.attributes).lock_version - p1.first_name = 'bianca2' + p1.first_name = "bianca2" p1.save! assert_equal 1, p1.lock_version assert_equal p1.lock_version, Person.new(p1.attributes).lock_version @@ -205,28 +235,144 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_lock_without_default_sets_version_to_zero t1 = LockWithoutDefault.new + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.save! + t1.reload + + assert_equal 0, t1.lock_version + assert_equal 0, t1.lock_version_before_type_cast + end + + def test_touch_existing_lock_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.touch + + assert_equal 1, t1.lock_version + end + + def test_touch_stale_object_with_lock_without_default + t1 = LockWithoutDefault.create!(title: "title1") + stale_object = LockWithoutDefault.find(t1.id) + + t1.update!(title: "title2") + + assert_raises(ActiveRecord::StaleObjectError) do + stale_object.touch + end + end + + def test_lock_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + t2 = LockWithoutDefault.find(t1.id) + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + assert_equal 0, t2.lock_version + assert_nil t2.lock_version_before_type_cast + + t1.title = "new title1" + t2.title = "new title2" + + assert_nothing_raised { t1.save! } + assert_equal 1, t1.lock_version + assert_equal "new title1", t1.title + + assert_raise(ActiveRecord::StaleObjectError) { t2.save! } + assert_equal 0, t2.lock_version + assert_equal "new title2", t2.title + end + + def test_lock_without_default_queries_count + t1 = LockWithoutDefault.create(title: "title1") - t1.save - t1 = LockWithoutDefault.find(t1.id) + assert_equal "title1", t1.title assert_equal 0, t1.lock_version + + assert_queries(1) { t1.update(title: "title2") } + + t1.reload + assert_equal "title2", t1.title + assert_equal 1, t1.lock_version + + t2 = LockWithoutDefault.new(title: "title1") + + assert_queries(1) { t2.save! } + + t2.reload + assert_equal "title1", t2.title + assert_equal 0, t2.lock_version end def test_lock_with_custom_column_without_default_sets_version_to_zero t1 = LockWithCustomColumnWithoutDefault.new + assert_equal 0, t1.custom_lock_version assert_nil t1.custom_lock_version_before_type_cast t1.save! t1.reload + + assert_equal 0, t1.custom_lock_version + assert_equal 0, t1.custom_lock_version_before_type_cast + end + + def test_lock_with_custom_column_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults_cust(title) VALUES('title1')") + + t1 = LockWithCustomColumnWithoutDefault.last + t2 = LockWithCustomColumnWithoutDefault.find(t1.id) + + assert_equal 0, t1.custom_lock_version + assert_nil t1.custom_lock_version_before_type_cast + assert_equal 0, t2.custom_lock_version + assert_nil t2.custom_lock_version_before_type_cast + + t1.title = "new title1" + t2.title = "new title2" + + assert_nothing_raised { t1.save! } + assert_equal 1, t1.custom_lock_version + assert_equal "new title1", t1.title + + assert_raise(ActiveRecord::StaleObjectError) { t2.save! } + assert_equal 0, t2.custom_lock_version + assert_equal "new title2", t2.title + end + + def test_lock_with_custom_column_without_default_queries_count + t1 = LockWithCustomColumnWithoutDefault.create(title: "title1") + + assert_equal "title1", t1.title assert_equal 0, t1.custom_lock_version - assert [0, "0"].include?(t1.custom_lock_version_before_type_cast) + + assert_queries(1) { t1.update(title: "title2") } + + t1.reload + assert_equal "title2", t1.title + assert_equal 1, t1.custom_lock_version + + t2 = LockWithCustomColumnWithoutDefault.new(title: "title1") + + assert_queries(1) { t2.save! } + + t2.reload + assert_equal "title1", t2.title + assert_equal 0, t2.custom_lock_version end def test_readonly_attributes - assert_equal Set.new([ 'name' ]), ReadonlyNameShip.readonly_attributes + assert_equal Set.new([ "name" ]), ReadonlyNameShip.readonly_attributes - s = ReadonlyNameShip.create(:name => "unchangeable name") + s = ReadonlyNameShip.create(name: "unchangeable name") s.reload assert_equal "unchangeable name", s.name @@ -245,7 +391,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase # is nothing else being updated. def test_update_without_attributes_does_not_only_update_lock_version assert_nothing_raised do - p1 = Person.create!(:first_name => 'anika') + p1 = Person.create!(first_name: "anika") lock_version = p1.lock_version p1.save p1.reload @@ -253,20 +399,52 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end + def test_counter_cache_with_touch_and_lock_version + car = Car.create! + + assert_equal 0, car.wheels_count + assert_equal 0, car.lock_version + + previously_car_updated_at = car.updated_at + travel(2.second) do + Wheel.create!(wheelable: car) + end + + assert_equal 1, car.reload.wheels_count + assert_not_equal previously_car_updated_at, car.updated_at + assert_equal 1, car.lock_version + + previously_car_updated_at = car.updated_at + car.wheels.first.update(size: 42) + + assert_equal 1, car.reload.wheels_count + assert_not_equal previously_car_updated_at, car.updated_at + assert_equal 2, car.lock_version + + previously_car_updated_at = car.updated_at + travel(2.second) do + car.wheels.first.destroy! + end + + assert_equal 0, car.reload.wheels_count + assert_not_equal previously_car_updated_at, car.updated_at + assert_equal 3, car.lock_version + end + def test_polymorphic_destroy_with_dependencies_and_lock_version car = Car.create! - assert_difference 'car.wheels.count' do - car.wheels << Wheel.create! + assert_difference "car.wheels.count" do + car.wheels.create end - assert_difference 'car.wheels.count', -1 do - car.destroy + assert_difference "car.wheels.count", -1 do + car.reload.destroy end assert car.destroyed? end def test_removing_has_and_belongs_to_many_associations_upon_destroy - p = RichPerson.create! first_name: 'Jon' + p = RichPerson.create! first_name: "Jon" p.treasures.create! assert !p.treasures.empty? p.destroy @@ -290,7 +468,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase # of a test (see test_increment_counter_*). self.use_transactional_tests = false - { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model| + { 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 @@ -305,7 +483,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase define_method("test_update_counters_updates_#{name}") do counter_test model, 1 do |id| - model.update_counters id, :test_count => 1 + model.update_counters id, test_count: 1 end end end @@ -313,13 +491,13 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase # See Lighthouse ticket #1966 def test_destroy_dependents # Establish dependent relationship between Person and PersonalLegacyThing - add_counter_column_to(Person, 'personal_legacy_things_count') + add_counter_column_to(Person, "personal_legacy_things_count") PersonalLegacyThing.reset_column_information # Make sure that counter incrementing doesn't cause problems - p1 = Person.new(:first_name => 'fjord') + p1 = Person.new(first_name: "fjord") p1.save! - t = PersonalLegacyThing.new(:person => p1) + t = PersonalLegacyThing.new(person: p1) t.save! p1.reload assert_equal 1, p1.personal_legacy_things_count @@ -328,14 +506,39 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) } assert_raises(ActiveRecord::RecordNotFound) { PersonalLegacyThing.find(t.id) } ensure - remove_counter_column_from(Person, 'personal_legacy_things_count') + remove_counter_column_from(Person, "personal_legacy_things_count") PersonalLegacyThing.reset_column_information end + def test_destroy_existing_object_with_locking_column_value_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.destroy + + assert t1.destroyed? + end + + def test_destroy_stale_object + t1 = LockWithoutDefault.create!(title: "title1") + stale_object = LockWithoutDefault.find(t1.id) + + t1.update!(title: "title2") + + assert_raises(ActiveRecord::StaleObjectError) do + stale_object.destroy! + end + + refute stale_object.destroyed? + end + private - def add_counter_column_to(model, col='test_count') - model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0 + def add_counter_column_to(model, col = "test_count") + model.connection.add_column model.table_name, col, :integer, null: false, default: 0 model.reset_column_information end @@ -358,7 +561,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase 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. @@ -395,34 +597,37 @@ unless in_memory_db? end end - # Locking a record reloads it. - def test_sane_lock_method + def test_lock_does_not_raise_when_the_object_is_not_dirty + person = Person.find 1 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 + person.lock! + end + end + + def test_lock_raises_when_the_record_is_dirty + person = Person.find 1 + person.first_name = "fooman" + assert_raises(RuntimeError) do + person.lock! end end def test_with_lock_commits_transaction person = Person.find 1 person.with_lock do - person.first_name = 'fooman' + person.first_name = "fooman" person.save! end - assert_equal 'fooman', person.reload.first_name + assert_equal "fooman", person.reload.first_name end def test_with_lock_rolls_back_transaction person = Person.find 1 old = person.first_name person.with_lock do - person.first_name = 'fooman' + person.first_name = "fooman" person.save! - raise 'oops' + raise "oops" end rescue nil assert_equal old, person.reload.first_name end @@ -431,47 +636,45 @@ unless in_memory_db? def test_lock_sending_custom_lock_statement Person.transaction do person = Person.find(1) - assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do - person.lock!('FOR SHARE NOWAIT') + assert_sql(/LIMIT \$?\d FOR SHARE NOWAIT/) do + person.lock!("FOR SHARE NOWAIT") end end end end - if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) - def test_no_locks_no_wait - first, second = duel { Person.find 1 } - assert first.end > second.end - end + def test_no_locks_no_wait + first, second = duel { Person.find 1 } + assert first.end > second.end + end - protected - def duel(zzz = 5) - 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 + private + def duel(zzz = 5) + t0, t1, t2, t3 = nil, nil, nil, nil - b = Thread.new do - sleep zzz / 2.0 # ensure thread 1 tx starts first - t2 = Time.now - Person.transaction { yield } - t3 = Time.now + a = Thread.new do + t0 = Time.now + Person.transaction do + yield + sleep zzz # block thread 2 for zzz seconds end + t1 = 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] + b = Thread.new do + sleep zzz / 2.0 # ensure thread 1 tx starts first + t2 = Time.now + Person.transaction { yield } + t3 = Time.now end - 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 |