I have a view that looks like this:
@transaction.commit_on_success
def view(request):
do_stuff(...)
if condition(...):
result = do_other_stuff(...)
transaction.commit()
send_email(result)
return HttpResponse(...)
Assume everything with (...) can touch the database.
There are currently two main cases:
- condition is true,
do_stuff()
anddo_other_stuff()
are done in transaction 1, the transaction is saved, then the e-mail is sent andHttpResponse
is done in transaction 2 - condition is false, everything happens in transaction 2
I want to rewrite this with transaction.atomic
. The important requirement is: the e-mail should only be sent if do_other_stuff()
really gets saved.
Obviously I can't use @atomic
around the whole view, because I wouldn't be able to commit, so my first shot looks something like:
def view(request):
should_send_email = False
with transaction.atomic:
do_stuff(...)
if condition(...):
result = do_other_stuff(...)
should_send_email = True
response = HttpResponse(...)
if should_send_email:
should_send_email(result)
return response
It's kind of acceptable, but it clobbers the control flow. How to rewrite it better?
I was thinking about something like:
def view(request):
with AtomicWithContinuation as a: # new context manager
do_stuff(...)
if condition(...):
result = do_other_stuff(...)
a.after_commit(lambda: send_email(result))
return HttpResponse(...)
which looks easier on the eyes, so I've rolled up a context manager like:
class AtomicWithContinuation(object):
def __init__(self, *args, **kwargs):
self.atomic = transaction.atomic(*args, **kwargs)
self._after_commit = []
self._after_rollback = []
def after_commit(self, cb):
self._after_commit.append(cb)
def after_rollback(self, cb):
self._after_rollback.append(cb)
def __enter__(self):
self.atomic.__enter__()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
try:
self.atomic.__exit__(exc_type, exc_val, exc_tb)
except Exception:
# atomic.__exit__ itself threw an exception
# - assume a rollback has been done
for cb in self._after_rollback:
cb()
raise
if exc_val is not None:
# Wrapped block raised an exception,
# so atomic had to do a rollback
for cb in self._after_rollback:
cb()
else:
for cb in self._after_commit:
cb()
Does this implementation make sense? Is this a good approach to take? Also, can I trust my assumptions in __exit__
?