Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions xact.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
""" This code provides a decorator / context manager for transaction management in
Django on PostgreSQL. It is intended as a replacement for the existing Django
commit_on_success() function, and provides some nice features:

* Nested transactions: The top-level transaction will be a BEGIN/COMMIT/ROLLBACK
block; inner "transactions" are implemented as savepoints.
* Commits even if is_dirty is False, eliminating the mistake of forgetting to set
the dirty flag when doing database-modifying raw SQL.
* Better interaction with pgPool II, if you're using it.
* A workaround for a subtle but nasty bug in Django's transaction management.

As currently implemented, it is NOT thread-safe as a decorator (it IS thread-safe
as a context manager). Fix coming.

Expand All @@ -17,27 +17,30 @@

from functools import wraps

from threading import local

from django.db import transaction, DEFAULT_DB_ALIAS, connections

import psycopg2.extensions

class _Transaction(object):
def __init__(self, using):
self.using = using
self.sid = None
self._local = local()

def __enter__(self):
if connections[self.using].features.uses_savepoints:
if transaction.is_managed(self.using):
# We're already in a transaction; create a savepoint.
self.sid = transaction.savepoint(self.using)
self._local.sid = transaction.savepoint(self.using)
else:
self._local.sid = None
transaction.enter_transaction_management(using=self.using)
transaction.managed(True, using=self.using)

def __exit__(self, exc_type, exc_value, traceback):
if exc_value is None:
# commit operation
if self.sid is None:
if self._local.sid is None:
# Outer transaction
try:
transaction.commit(self.using)
Expand All @@ -49,20 +52,20 @@ def __exit__(self, exc_type, exc_value, traceback):
else:
# Inner savepoint
try:
transaction.savepoint_commit(self.sid, self.using)
transaction.savepoint_commit(self._local.sid, self.using)
except:
transaction.savepoint_rollback(self.sid, self.using)
transaction.savepoint_rollback(self._local.sid, self.using)
raise
else:
# rollback operation
if self.sid is None:
if self._local.sid is None:
# Outer transaction
transaction.rollback(self.using)
self._leave_transaction_management()
else:
# Inner savepoint
transaction.savepoint_rollback(self.sid, self.using)
transaction.savepoint_rollback(self._local.sid, self.using)

return False

def _leave_transaction_management(self):
Expand All @@ -71,10 +74,10 @@ def _leave_transaction_management(self):
connections[self.using]._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
# Patch for bug in Django's psycopg2 backend; see:
# https://code.djangoproject.com/ticket/16047

# This is a great recipe for allowing a single object to handle both @ decorators
# and with contexts.

def __call__(self, func):
@wraps(func)
def inner(*args, **kwargs):
Expand Down