Skip to content

Commit cced863

Browse files
committed
INTPYTHON-355 Add transaction.atomic()
1 parent 0deeb1d commit cced863

File tree

4 files changed

+697
-0
lines changed

4 files changed

+697
-0
lines changed

django_mongodb_backend/transaction.py

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

tests/transactions_/__init__.py

Whitespace-only changes.

tests/transactions_/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""
2+
Transactions
3+
4+
Django handles transactions in three different ways. The default is to commit
5+
each transaction upon a write, but you can decorate a function to get
6+
commit-on-success behavior. Alternatively, you can manage the transaction
7+
manually.
8+
"""
9+
10+
from django.db import models
11+
12+
13+
class Reporter(models.Model):
14+
first_name = models.CharField(max_length=30)
15+
last_name = models.CharField(max_length=30)
16+
email = models.EmailField()
17+
18+
class Meta:
19+
ordering = ("first_name", "last_name")
20+
21+
def __str__(self):
22+
return f"{self.first_name} {self.last_name}".strip()

0 commit comments

Comments
 (0)