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_other_stuff()are done in transaction 1, the transaction is saved, then the e-mail is sent and
HttpResponseis 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