tests.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. from __future__ import absolute_import
  2. import sys
  3. import time
  4. import unittest
  5. from django.conf import settings
  6. from django.db import transaction, connection
  7. from django.db.utils import ConnectionHandler, DEFAULT_DB_ALIAS, DatabaseError
  8. from django.test import (TransactionTestCase, skipIfDBFeature,
  9. skipUnlessDBFeature)
  10. from .models import Person
  11. # Some tests require threading, which might not be available. So create a
  12. # skip-test decorator for those test functions.
  13. try:
  14. import threading
  15. except ImportError:
  16. threading = None
  17. requires_threading = unittest.skipUnless(threading, 'requires threading')
  18. class SelectForUpdateTests(TransactionTestCase):
  19. available_apps = ['select_for_update']
  20. def setUp(self):
  21. transaction.enter_transaction_management()
  22. self.person = Person.objects.create(name='Reinhardt')
  23. # We have to commit here so that code in run_select_for_update can
  24. # see this data.
  25. transaction.commit()
  26. # We need another database connection to test that one connection
  27. # issuing a SELECT ... FOR UPDATE will block.
  28. new_connections = ConnectionHandler(settings.DATABASES)
  29. self.new_connection = new_connections[DEFAULT_DB_ALIAS]
  30. self.new_connection.enter_transaction_management()
  31. # We need to set settings.DEBUG to True so we can capture
  32. # the output SQL to examine.
  33. self._old_debug = settings.DEBUG
  34. settings.DEBUG = True
  35. def tearDown(self):
  36. try:
  37. # We don't really care if this fails - some of the tests will set
  38. # this in the course of their run.
  39. transaction.abort()
  40. self.new_connection.abort()
  41. except transaction.TransactionManagementError:
  42. pass
  43. self.new_connection.close()
  44. settings.DEBUG = self._old_debug
  45. try:
  46. self.end_blocking_transaction()
  47. except (DatabaseError, AttributeError):
  48. pass
  49. def start_blocking_transaction(self):
  50. # Start a blocking transaction. At some point,
  51. # end_blocking_transaction() should be called.
  52. self.cursor = self.new_connection.cursor()
  53. sql = 'SELECT * FROM %(db_table)s %(for_update)s;' % {
  54. 'db_table': Person._meta.db_table,
  55. 'for_update': self.new_connection.ops.for_update_sql(),
  56. }
  57. self.cursor.execute(sql, ())
  58. self.cursor.fetchone()
  59. def end_blocking_transaction(self):
  60. # Roll back the blocking transaction.
  61. self.new_connection.rollback()
  62. def has_for_update_sql(self, tested_connection, nowait=False):
  63. # Examine the SQL that was executed to determine whether it
  64. # contains the 'SELECT..FOR UPDATE' stanza.
  65. for_update_sql = tested_connection.ops.for_update_sql(nowait)
  66. sql = tested_connection.queries[-1]['sql']
  67. return bool(sql.find(for_update_sql) > -1)
  68. @skipUnlessDBFeature('has_select_for_update')
  69. def test_for_update_sql_generated(self):
  70. """
  71. Test that the backend's FOR UPDATE variant appears in
  72. generated SQL when select_for_update is invoked.
  73. """
  74. list(Person.objects.all().select_for_update())
  75. self.assertTrue(self.has_for_update_sql(connection))
  76. @skipUnlessDBFeature('has_select_for_update_nowait')
  77. def test_for_update_sql_generated_nowait(self):
  78. """
  79. Test that the backend's FOR UPDATE NOWAIT variant appears in
  80. generated SQL when select_for_update is invoked.
  81. """
  82. list(Person.objects.all().select_for_update(nowait=True))
  83. self.assertTrue(self.has_for_update_sql(connection, nowait=True))
  84. @requires_threading
  85. @skipUnlessDBFeature('has_select_for_update_nowait')
  86. def test_nowait_raises_error_on_block(self):
  87. """
  88. If nowait is specified, we expect an error to be raised rather
  89. than blocking.
  90. """
  91. self.start_blocking_transaction()
  92. status = []
  93. thread = threading.Thread(
  94. target=self.run_select_for_update,
  95. args=(status,),
  96. kwargs={'nowait': True},
  97. )
  98. thread.start()
  99. time.sleep(1)
  100. thread.join()
  101. self.end_blocking_transaction()
  102. self.assertIsInstance(status[-1], DatabaseError)
  103. @skipIfDBFeature('has_select_for_update_nowait')
  104. @skipUnlessDBFeature('has_select_for_update')
  105. def test_unsupported_nowait_raises_error(self):
  106. """
  107. If a SELECT...FOR UPDATE NOWAIT is run on a database backend
  108. that supports FOR UPDATE but not NOWAIT, then we should find
  109. that a DatabaseError is raised.
  110. """
  111. self.assertRaises(
  112. DatabaseError,
  113. list,
  114. Person.objects.all().select_for_update(nowait=True)
  115. )
  116. def run_select_for_update(self, status, nowait=False):
  117. """
  118. Utility method that runs a SELECT FOR UPDATE against all
  119. Person instances. After the select_for_update, it attempts
  120. to update the name of the only record, save, and commit.
  121. This function expects to run in a separate thread.
  122. """
  123. status.append('started')
  124. try:
  125. # We need to enter transaction management again, as this is done on
  126. # per-thread basis
  127. transaction.enter_transaction_management()
  128. people = list(
  129. Person.objects.all().select_for_update(nowait=nowait)
  130. )
  131. people[0].name = 'Fred'
  132. people[0].save()
  133. transaction.commit()
  134. except DatabaseError as e:
  135. status.append(e)
  136. finally:
  137. # This method is run in a separate thread. It uses its own
  138. # database connection. Close it without waiting for the GC.
  139. transaction.abort()
  140. connection.close()
  141. @requires_threading
  142. @skipUnlessDBFeature('has_select_for_update')
  143. @skipUnlessDBFeature('supports_transactions')
  144. def test_block(self):
  145. """
  146. Check that a thread running a select_for_update that
  147. accesses rows being touched by a similar operation
  148. on another connection blocks correctly.
  149. """
  150. # First, let's start the transaction in our thread.
  151. self.start_blocking_transaction()
  152. # Now, try it again using the ORM's select_for_update
  153. # facility. Do this in a separate thread.
  154. status = []
  155. thread = threading.Thread(
  156. target=self.run_select_for_update, args=(status,)
  157. )
  158. # The thread should immediately block, but we'll sleep
  159. # for a bit to make sure.
  160. thread.start()
  161. sanity_count = 0
  162. while len(status) != 1 and sanity_count < 10:
  163. sanity_count += 1
  164. time.sleep(1)
  165. if sanity_count >= 10:
  166. raise ValueError('Thread did not run and block')
  167. # Check the person hasn't been updated. Since this isn't
  168. # using FOR UPDATE, it won't block.
  169. p = Person.objects.get(pk=self.person.pk)
  170. self.assertEqual('Reinhardt', p.name)
  171. # When we end our blocking transaction, our thread should
  172. # be able to continue.
  173. self.end_blocking_transaction()
  174. thread.join(5.0)
  175. # Check the thread has finished. Assuming it has, we should
  176. # find that it has updated the person's name.
  177. self.assertFalse(thread.isAlive())
  178. # We must commit the transaction to ensure that MySQL gets a fresh read,
  179. # since by default it runs in REPEATABLE READ mode
  180. transaction.commit()
  181. p = Person.objects.get(pk=self.person.pk)
  182. self.assertEqual('Fred', p.name)
  183. @requires_threading
  184. @skipUnlessDBFeature('has_select_for_update')
  185. def test_raw_lock_not_available(self):
  186. """
  187. Check that running a raw query which can't obtain a FOR UPDATE lock
  188. raises the correct exception
  189. """
  190. self.start_blocking_transaction()
  191. def raw(status):
  192. try:
  193. list(
  194. Person.objects.raw(
  195. 'SELECT * FROM %s %s' % (
  196. Person._meta.db_table,
  197. connection.ops.for_update_sql(nowait=True)
  198. )
  199. )
  200. )
  201. except DatabaseError as e:
  202. status.append(e)
  203. finally:
  204. # This method is run in a separate thread. It uses its own
  205. # database connection. Close it without waiting for the GC.
  206. connection.close()
  207. status = []
  208. thread = threading.Thread(target=raw, kwargs={'status': status})
  209. thread.start()
  210. time.sleep(1)
  211. thread.join()
  212. self.end_blocking_transaction()
  213. self.assertIsInstance(status[-1], DatabaseError)
  214. @skipUnlessDBFeature('has_select_for_update')
  215. def test_transaction_dirty_managed(self):
  216. """ Check that a select_for_update sets the transaction to be
  217. dirty when executed under txn management. Setting the txn dirty
  218. means that it will be either committed or rolled back by Django,
  219. which will release any locks held by the SELECT FOR UPDATE.
  220. """
  221. people = list(Person.objects.select_for_update())
  222. self.assertTrue(transaction.is_dirty())