aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/locking_test.rb
blob: 35c557e19db589ed3f0c8f9b5c4e532310465ecb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
require 'abstract_unit'
require 'fixtures/person'
require 'fixtures/legacy_thing'

class OptimisticLockingTest < Test::Unit::TestCase
  fixtures :people, :legacy_things

  def test_lock_existing
    p1 = Person.find(1)
    p2 = Person.find(1)
    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! }
  end

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

    p1.save!
    p2 = Person.find(p1.id)
    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! }
  end

  def test_lock_column_name_existing
    t1 = LegacyThing.find(1)
    t2 = LegacyThing.find(1)
    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

    p1.save!
    assert_equal 1, p1.lock_version
    assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
  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

  # 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
  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