locking_test.rb 4.9 KB
Newer Older
1 2
require 'abstract_unit'
require 'fixtures/person'
3
require 'fixtures/legacy_thing'
4

5 6 7
class LockWithoutDefault < ActiveRecord::Base; end

class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
8
  set_table_name :lock_without_defaults_cust
9 10 11
  set_locking_column :custom_lock_version
end

12
class OptimisticLockingTest < Test::Unit::TestCase
13
  fixtures :people, :legacy_things
14 15 16 17

  def test_lock_existing
    p1 = Person.find(1)
    p2 = Person.find(1)
18 19
    assert_equal 0, p1.lock_version
    assert_equal 0, p2.lock_version
20

21 22 23
    p1.save!
    assert_equal 1, p1.lock_version
    assert_equal 0, p2.lock_version
24

25
    assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
26 27 28
  end

  def test_lock_new
29 30 31 32
    p1 = Person.new(:first_name => 'anika')
    assert_equal 0, p1.lock_version

    p1.save!
33
    p2 = Person.find(p1.id)
34 35 36 37 38 39 40 41
    assert_equal 0, p1.lock_version
    assert_equal 0, p2.lock_version

    p1.save!
    assert_equal 1, p1.lock_version
    assert_equal 0, p2.lock_version

    assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
42
  end
43

44 45 46
  def test_lock_column_name_existing
    t1 = LegacyThing.find(1)
    t2 = LegacyThing.find(1)
47 48 49 50 51 52 53 54 55 56 57 58 59 60
    assert_equal 0, t1.version
    assert_equal 0, t2.version

    t1.save!
    assert_equal 1, t1.version
    assert_equal 0, t2.version

    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
61

62 63 64
    p1.save!
    assert_equal 1, p1.lock_version
    assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
65
  end
66

67 68 69 70
  def test_lock_without_default_sets_version_to_zero
    t1 = LockWithoutDefault.new
    assert_equal 0, t1.lock_version
  end
71

72 73 74 75
  def test_lock_with_custom_column_without_default_sets_version_to_zero
    t1 = LockWithCustomColumnWithoutDefault.new
    assert_equal 0, t1.custom_lock_version
  end
76 77 78 79 80 81 82 83
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.)

84
# TODO: The SQL Server adapter currently has no support for pessimistic locking
85

86 87 88
unless current_adapter?(:SQLServerAdapter)
  class PessimisticLockingTest < Test::Unit::TestCase
    self.use_transactional_fixtures = false
89
    fixtures :people, :readers
90

91
    def setup
92 93 94
      # Avoid introspection queries during tests.
      Person.columns; Reader.columns

95 96
      @allow_concurrency = ActiveRecord::Base.allow_concurrency
      ActiveRecord::Base.allow_concurrency = true
97
    end
98

99 100 101
    def teardown
      ActiveRecord::Base.allow_concurrency = @allow_concurrency
    end
102 103

    # Test typical find.
104 105 106 107
    def test_sane_find_with_lock
      assert_nothing_raised do
        Person.transaction do
          Person.find 1, :lock => true
108 109 110
        end
      end
    end
111 112 113

    # Test scoped lock.
    def test_sane_find_with_scoped_lock
114 115 116 117 118 119
      assert_nothing_raised do
        Person.transaction do
          Person.with_scope(:find => { :lock => true }) do
            Person.find 1
          end
        end
120 121
      end
    end
122 123 124 125 126 127 128

    # 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
129
            Person.find 1, :include => :readers, :lock => true
130 131 132 133 134
          end
        end
      end
    end

135 136 137 138 139 140 141 142 143 144
    # 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
145
    end
146

147 148 149 150 151
    if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
      def test_no_locks_no_wait
        first, second = duel { Person.find 1 }
        assert first.end > second.end
      end
152

153 154 155 156 157 158
      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
159

160 161 162
      protected
        def duel(zzz = 5)
          t0, t1, t2, t3 = nil, nil, nil, nil
163

164 165 166 167 168 169 170
          a = Thread.new do
            t0 = Time.now
            Person.transaction do
              yield
              sleep zzz       # block thread 2 for zzz seconds
            end
            t1 = Time.now
171
          end
172

173 174 175 176 177 178
          b = Thread.new do
            sleep zzz / 2.0   # ensure thread 1 tx starts first
            t2 = Time.now
            Person.transaction { yield }
            t3 = Time.now
          end
179

180 181
          a.join
          b.join
182

183 184 185 186
          assert t1 > t0 + zzz
          assert t2 > t0
          assert t3 > t2
          [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
187
        end
188
    end
189
  end
190
end