You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

283 lines
7.8KB

  1. require 'abstract_unit'
  2. require 'fixtures/person'
  3. require 'fixtures/reader'
  4. require 'fixtures/legacy_thing'
  5. class LockWithoutDefault < ActiveRecord::Base; end
  6. class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
  7. set_table_name :lock_without_defaults_cust
  8. set_locking_column :custom_lock_version
  9. end
  10. class ReadonlyFirstNamePerson < Person
  11. attr_readonly :first_name
  12. end
  13. class OptimisticLockingTest < Test::Unit::TestCase
  14. fixtures :people, :legacy_things
  15. # need to disable transactional fixtures, because otherwise the sqlite3
  16. # adapter (at least) chokes when we try and change the schema in the middle
  17. # of a test (see test_increment_counter_*).
  18. self.use_transactional_fixtures = false
  19. def test_lock_existing
  20. p1 = Person.find(1)
  21. p2 = Person.find(1)
  22. assert_equal 0, p1.lock_version
  23. assert_equal 0, p2.lock_version
  24. p1.save!
  25. assert_equal 1, p1.lock_version
  26. assert_equal 0, p2.lock_version
  27. assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
  28. end
  29. def test_lock_repeating
  30. p1 = Person.find(1)
  31. p2 = Person.find(1)
  32. assert_equal 0, p1.lock_version
  33. assert_equal 0, p2.lock_version
  34. p1.save!
  35. assert_equal 1, p1.lock_version
  36. assert_equal 0, p2.lock_version
  37. assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
  38. assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
  39. end
  40. def test_lock_new
  41. p1 = Person.new(:first_name => 'anika')
  42. assert_equal 0, p1.lock_version
  43. p1.save!
  44. p2 = Person.find(p1.id)
  45. assert_equal 0, p1.lock_version
  46. assert_equal 0, p2.lock_version
  47. p1.save!
  48. assert_equal 1, p1.lock_version
  49. assert_equal 0, p2.lock_version
  50. assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
  51. end
  52. def test_lock_new_with_nil
  53. p1 = Person.new(:first_name => 'anika')
  54. p1.save!
  55. p1.lock_version = nil # simulate bad fixture or column with no default
  56. p1.save!
  57. assert_equal 1, p1.lock_version
  58. end
  59. def test_lock_column_name_existing
  60. t1 = LegacyThing.find(1)
  61. t2 = LegacyThing.find(1)
  62. assert_equal 0, t1.version
  63. assert_equal 0, t2.version
  64. t1.save!
  65. assert_equal 1, t1.version
  66. assert_equal 0, t2.version
  67. assert_raises(ActiveRecord::StaleObjectError) { t2.save! }
  68. end
  69. def test_lock_column_is_mass_assignable
  70. p1 = Person.create(:first_name => 'bianca')
  71. assert_equal 0, p1.lock_version
  72. assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
  73. p1.save!
  74. assert_equal 1, p1.lock_version
  75. assert_equal p1.lock_version, Person.new(p1.attributes).lock_version
  76. end
  77. def test_lock_without_default_sets_version_to_zero
  78. t1 = LockWithoutDefault.new
  79. assert_equal 0, t1.lock_version
  80. end
  81. def test_lock_with_custom_column_without_default_sets_version_to_zero
  82. t1 = LockWithCustomColumnWithoutDefault.new
  83. assert_equal 0, t1.custom_lock_version
  84. end
  85. def test_readonly_attributes
  86. assert_equal Set.new([ 'first_name' ]), ReadonlyFirstNamePerson.readonly_attributes
  87. p = ReadonlyFirstNamePerson.create(:first_name => "unchangeable name")
  88. p.reload
  89. assert_equal "unchangeable name", p.first_name
  90. p.update_attributes(:first_name => "changed name")
  91. p.reload
  92. assert_equal "unchangeable name", p.first_name
  93. end
  94. { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model|
  95. define_method("test_increment_counter_updates_#{name}") do
  96. counter_test model, 1 do |id|
  97. model.increment_counter :test_count, id
  98. end
  99. end
  100. define_method("test_decrement_counter_updates_#{name}") do
  101. counter_test model, -1 do |id|
  102. model.decrement_counter :test_count, id
  103. end
  104. end
  105. define_method("test_update_counters_updates_#{name}") do
  106. counter_test model, 1 do |id|
  107. model.update_counters id, :test_count => 1
  108. end
  109. end
  110. end
  111. private
  112. def add_counter_column_to(model)
  113. model.connection.add_column model.table_name, :test_count, :integer, :null => false, :default => 0
  114. model.reset_column_information
  115. # OpenBase does not set a value to existing rows when adding a not null default column
  116. model.update_all(:test_count => 0) if current_adapter?(:OpenBaseAdapter)
  117. end
  118. def remove_counter_column_from(model)
  119. model.connection.remove_column model.table_name, :test_count
  120. model.reset_column_information
  121. end
  122. def counter_test(model, expected_count)
  123. add_counter_column_to(model)
  124. object = model.find(:first)
  125. assert_equal 0, object.test_count
  126. assert_equal 0, object.send(model.locking_column)
  127. yield object.id
  128. object.reload
  129. assert_equal expected_count, object.test_count
  130. assert_equal 1, object.send(model.locking_column)
  131. ensure
  132. remove_counter_column_from(model)
  133. end
  134. end
  135. # TODO: test against the generated SQL since testing locking behavior itself
  136. # is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
  137. # blocks, so separate script called by Kernel#system is needed.
  138. # (See exec vs. async_exec in the PostgreSQL adapter.)
  139. # TODO: The SQL Server, Sybase, and OpenBase adapters currently have no support for pessimistic locking
  140. unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter)
  141. class PessimisticLockingTest < Test::Unit::TestCase
  142. self.use_transactional_fixtures = false
  143. fixtures :people, :readers
  144. def setup
  145. # Avoid introspection queries during tests.
  146. Person.columns; Reader.columns
  147. @allow_concurrency = ActiveRecord::Base.allow_concurrency
  148. ActiveRecord::Base.allow_concurrency = true
  149. end
  150. def teardown
  151. ActiveRecord::Base.allow_concurrency = @allow_concurrency
  152. end
  153. # Test typical find.
  154. def test_sane_find_with_lock
  155. assert_nothing_raised do
  156. Person.transaction do
  157. Person.find 1, :lock => true
  158. end
  159. end
  160. end
  161. # Test scoped lock.
  162. def test_sane_find_with_scoped_lock
  163. assert_nothing_raised do
  164. Person.transaction do
  165. Person.with_scope(:find => { :lock => true }) do
  166. Person.find 1
  167. end
  168. end
  169. end
  170. end
  171. # PostgreSQL protests SELECT ... FOR UPDATE on an outer join.
  172. unless current_adapter?(:PostgreSQLAdapter)
  173. # Test locked eager find.
  174. def test_eager_find_with_lock
  175. assert_nothing_raised do
  176. Person.transaction do
  177. Person.find 1, :include => :readers, :lock => true
  178. end
  179. end
  180. end
  181. end
  182. # Locking a record reloads it.
  183. def test_sane_lock_method
  184. assert_nothing_raised do
  185. Person.transaction do
  186. person = Person.find 1
  187. old, person.first_name = person.first_name, 'fooman'
  188. person.lock!
  189. assert_equal old, person.first_name
  190. end
  191. end
  192. end
  193. if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
  194. def test_no_locks_no_wait
  195. first, second = duel { Person.find 1 }
  196. assert first.end > second.end
  197. end
  198. def test_second_lock_waits
  199. assert [0.2, 1, 5].any? { |zzz|
  200. first, second = duel(zzz) { Person.find 1, :lock => true }
  201. second.end > first.end
  202. }
  203. end
  204. protected
  205. def duel(zzz = 5)
  206. t0, t1, t2, t3 = nil, nil, nil, nil
  207. a = Thread.new do
  208. t0 = Time.now
  209. Person.transaction do
  210. yield
  211. sleep zzz # block thread 2 for zzz seconds
  212. end
  213. t1 = Time.now
  214. end
  215. b = Thread.new do
  216. sleep zzz / 2.0 # ensure thread 1 tx starts first
  217. t2 = Time.now
  218. Person.transaction { yield }
  219. t3 = Time.now
  220. end
  221. a.join
  222. b.join
  223. assert t1 > t0 + zzz
  224. assert t2 > t0
  225. assert t3 > t2
  226. [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
  227. end
  228. end
  229. end
  230. end