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.

transaction.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. from contextlib import ContextDecorator, contextmanager
  2. from django.db import (
  3. DEFAULT_DB_ALIAS, DatabaseError, Error, ProgrammingError, connections,
  4. )
  5. class TransactionManagementError(ProgrammingError):
  6. """Transaction management is used improperly."""
  7. pass
  8. def get_connection(using=None):
  9. """
  10. Get a database connection by name, or the default database connection
  11. if no name is provided. This is a private API.
  12. """
  13. if using is None:
  14. using = DEFAULT_DB_ALIAS
  15. return connections[using]
  16. def get_autocommit(using=None):
  17. """Get the autocommit status of the connection."""
  18. return get_connection(using).get_autocommit()
  19. def set_autocommit(autocommit, using=None):
  20. """Set the autocommit status of the connection."""
  21. return get_connection(using).set_autocommit(autocommit)
  22. def commit(using=None):
  23. """Commit a transaction."""
  24. get_connection(using).commit()
  25. def rollback(using=None):
  26. """Roll back a transaction."""
  27. get_connection(using).rollback()
  28. def savepoint(using=None):
  29. """
  30. Create a savepoint (if supported and required by the backend) inside the
  31. current transaction. Return an identifier for the savepoint that will be
  32. used for the subsequent rollback or commit.
  33. """
  34. return get_connection(using).savepoint()
  35. def savepoint_rollback(sid, using=None):
  36. """
  37. Roll back the most recent savepoint (if one exists). Do nothing if
  38. savepoints are not supported.
  39. """
  40. get_connection(using).savepoint_rollback(sid)
  41. def savepoint_commit(sid, using=None):
  42. """
  43. Commit the most recent savepoint (if one exists). Do nothing if
  44. savepoints are not supported.
  45. """
  46. get_connection(using).savepoint_commit(sid)
  47. def clean_savepoints(using=None):
  48. """
  49. Reset the counter used to generate unique savepoint ids in this thread.
  50. """
  51. get_connection(using).clean_savepoints()
  52. def get_rollback(using=None):
  53. """Get the "needs rollback" flag -- for *advanced use* only."""
  54. return get_connection(using).get_rollback()
  55. def set_rollback(rollback, using=None):
  56. """
  57. Set or unset the "needs rollback" flag -- for *advanced use* only.
  58. When `rollback` is `True`, trigger a rollback when exiting the innermost
  59. enclosing atomic block that has `savepoint=True` (that's the default). Use
  60. this to force a rollback without raising an exception.
  61. When `rollback` is `False`, prevent such a rollback. Use this only after
  62. rolling back to a known-good state! Otherwise, you break the atomic block
  63. and data corruption may occur.
  64. """
  65. return get_connection(using).set_rollback(rollback)
  66. @contextmanager
  67. def mark_for_rollback_on_error(using=None):
  68. """
  69. Internal low-level utility to mark a transaction as "needs rollback" when
  70. an exception is raised while not enforcing the enclosed block to be in a
  71. transaction. This is needed by Model.save() and friends to avoid starting a
  72. transaction when in autocommit mode and a single query is executed.
  73. It's equivalent to:
  74. connection = get_connection(using)
  75. if connection.get_autocommit():
  76. yield
  77. else:
  78. with transaction.atomic(using=using, savepoint=False):
  79. yield
  80. but it uses low-level utilities to avoid performance overhead.
  81. """
  82. try:
  83. yield
  84. except Exception:
  85. connection = get_connection(using)
  86. if connection.in_atomic_block:
  87. connection.needs_rollback = True
  88. raise
  89. def on_commit(func, using=None):
  90. """
  91. Register `func` to be called when the current transaction is committed.
  92. If the current transaction is rolled back, `func` will not be called.
  93. """
  94. get_connection(using).on_commit(func)
  95. #################################
  96. # Decorators / context managers #
  97. #################################
  98. class Atomic(ContextDecorator):
  99. """
  100. Guarantee the atomic execution of a given block.
  101. An instance can be used either as a decorator or as a context manager.
  102. When it's used as a decorator, __call__ wraps the execution of the
  103. decorated function in the instance itself, used as a context manager.
  104. When it's used as a context manager, __enter__ creates a transaction or a
  105. savepoint, depending on whether a transaction is already in progress, and
  106. __exit__ commits the transaction or releases the savepoint on normal exit,
  107. and rolls back the transaction or to the savepoint on exceptions.
  108. It's possible to disable the creation of savepoints if the goal is to
  109. ensure that some code runs within a transaction without creating overhead.
  110. A stack of savepoints identifiers is maintained as an attribute of the
  111. connection. None denotes the absence of a savepoint.
  112. This allows reentrancy even if the same AtomicWrapper is reused. For
  113. example, it's possible to define `oa = atomic('other')` and use `@oa` or
  114. `with oa:` multiple times.
  115. Since database connections are thread-local, this is thread-safe.
  116. This is a private API.
  117. """
  118. def __init__(self, using, savepoint):
  119. self.using = using
  120. self.savepoint = savepoint
  121. def __enter__(self):
  122. connection = get_connection(self.using)
  123. if not connection.in_atomic_block:
  124. # Reset state when entering an outermost atomic block.
  125. connection.commit_on_exit = True
  126. connection.needs_rollback = False
  127. if not connection.get_autocommit():
  128. # sqlite3 in Python < 3.6 doesn't handle transactions and
  129. # savepoints properly when autocommit is off.
  130. # Turning autocommit back on isn't an option; it would trigger
  131. # a premature commit. Give up if that happens.
  132. if connection.features.autocommits_when_autocommit_is_off:
  133. raise TransactionManagementError(
  134. "Your database backend doesn't behave properly when "
  135. "autocommit is off. Turn it on before using 'atomic'.")
  136. # Pretend we're already in an atomic block to bypass the code
  137. # that disables autocommit to enter a transaction, and make a
  138. # note to deal with this case in __exit__.
  139. connection.in_atomic_block = True
  140. connection.commit_on_exit = False
  141. if connection.in_atomic_block:
  142. # We're already in a transaction; create a savepoint, unless we
  143. # were told not to or we're already waiting for a rollback. The
  144. # second condition avoids creating useless savepoints and prevents
  145. # overwriting needs_rollback until the rollback is performed.
  146. if self.savepoint and not connection.needs_rollback:
  147. sid = connection.savepoint()
  148. connection.savepoint_ids.append(sid)
  149. else:
  150. connection.savepoint_ids.append(None)
  151. else:
  152. connection.set_autocommit(False, force_begin_transaction_with_broken_autocommit=True)
  153. connection.in_atomic_block = True
  154. def __exit__(self, exc_type, exc_value, traceback):
  155. connection = get_connection(self.using)
  156. if connection.savepoint_ids:
  157. sid = connection.savepoint_ids.pop()
  158. else:
  159. # Prematurely unset this flag to allow using commit or rollback.
  160. connection.in_atomic_block = False
  161. try:
  162. if connection.closed_in_transaction:
  163. # The database will perform a rollback by itself.
  164. # Wait until we exit the outermost block.
  165. pass
  166. elif exc_type is None and not connection.needs_rollback:
  167. if connection.in_atomic_block:
  168. # Release savepoint if there is one
  169. if sid is not None:
  170. try:
  171. connection.savepoint_commit(sid)
  172. except DatabaseError:
  173. try:
  174. connection.savepoint_rollback(sid)
  175. # The savepoint won't be reused. Release it to
  176. # minimize overhead for the database server.
  177. connection.savepoint_commit(sid)
  178. except Error:
  179. # If rolling back to a savepoint fails, mark for
  180. # rollback at a higher level and avoid shadowing
  181. # the original exception.
  182. connection.needs_rollback = True
  183. raise
  184. else:
  185. # Commit transaction
  186. try:
  187. connection.commit()
  188. except DatabaseError:
  189. try:
  190. connection.rollback()
  191. except Error:
  192. # An error during rollback means that something
  193. # went wrong with the connection. Drop it.
  194. connection.close()
  195. raise
  196. else:
  197. # This flag will be set to True again if there isn't a savepoint
  198. # allowing to perform the rollback at this level.
  199. connection.needs_rollback = False
  200. if connection.in_atomic_block:
  201. # Roll back to savepoint if there is one, mark for rollback
  202. # otherwise.
  203. if sid is None:
  204. connection.needs_rollback = True
  205. else:
  206. try:
  207. connection.savepoint_rollback(sid)
  208. # The savepoint won't be reused. Release it to
  209. # minimize overhead for the database server.
  210. connection.savepoint_commit(sid)
  211. except Error:
  212. # If rolling back to a savepoint fails, mark for
  213. # rollback at a higher level and avoid shadowing
  214. # the original exception.
  215. connection.needs_rollback = True
  216. else:
  217. # Roll back transaction
  218. try:
  219. connection.rollback()
  220. except Error:
  221. # An error during rollback means that something
  222. # went wrong with the connection. Drop it.
  223. connection.close()
  224. finally:
  225. # Outermost block exit when autocommit was enabled.
  226. if not connection.in_atomic_block:
  227. if connection.closed_in_transaction:
  228. connection.connection = None
  229. else:
  230. connection.set_autocommit(True)
  231. # Outermost block exit when autocommit was disabled.
  232. elif not connection.savepoint_ids and not connection.commit_on_exit:
  233. if connection.closed_in_transaction:
  234. connection.connection = None
  235. else:
  236. connection.in_atomic_block = False
  237. def atomic(using=None, savepoint=True):
  238. # Bare decorator: @atomic -- although the first argument is called
  239. # `using`, it's actually the function being decorated.
  240. if callable(using):
  241. return Atomic(DEFAULT_DB_ALIAS, savepoint)(using)
  242. # Decorator: @atomic(...) or context manager: with atomic(...): ...
  243. else:
  244. return Atomic(using, savepoint)
  245. def _non_atomic_requests(view, using):
  246. try:
  247. view._non_atomic_requests.add(using)
  248. except AttributeError:
  249. view._non_atomic_requests = {using}
  250. return view
  251. def non_atomic_requests(using=None):
  252. if callable(using):
  253. return _non_atomic_requests(using, DEFAULT_DB_ALIAS)
  254. else:
  255. if using is None:
  256. using = DEFAULT_DB_ALIAS
  257. return lambda view: _non_atomic_requests(view, using)