Browse files

Multi-thread and reentrant-safe.

  • Loading branch information...
1 parent 23d3ffa commit 5beefd104af48f1516679f3ab179f543aad0ad26 @Xof committed Jun 6, 2012
Showing with 64 additions and 12 deletions.
  1. +64 −12 xact.py
View
76 xact.py
@@ -9,25 +9,29 @@
* 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.
-
For full details, check the README.md file.
"""
from functools import wraps
-from django.db import transaction, DEFAULT_DB_ALIAS, connections
-
import psycopg2.extensions
+from django.db import transaction, DEFAULT_DB_ALIAS, connections
+
class _Transaction(object):
+
+ """ This class manages a particular transaction or savepoint block, using context
+ manager-style __enter__ and __exit__ statements. We don't use it directly
+ (for reasons noted below), but as a delegate for the _TransactionWrapper
+ class.
+ """
+
def __init__(self, using):
self.using = using
self.sid = None
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)
else:
@@ -64,21 +68,55 @@ def __exit__(self, exc_type, exc_value, traceback):
transaction.savepoint_rollback(self.sid, self.using)
return False
-
+ # Returning False here means we did not gobble up the exception, so the
+ # exception process should continue.
+
def _leave_transaction_management(self):
transaction.leave_transaction_management(using=self.using)
if not connections[self.using].is_managed() and connections[self.using].features.uses_autocommit:
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.
+
+class _TransactionWrapper:
+
+ """ This class wraps the above _Transaction class. We do this to allow reentrancy
+ and thread-safety. When being used as a decorator, only one _TransactionWrapper
+ object is created per function being wrapped, and thus we can't store the state
+ of the tranasction here (because multiple concurrent calls in the same address
+ space to the same function would cause the state to be crunched), so delegate
+ that to a _Transaction object that is created at the appropriate time.
+
+ The "appropriate time" is two places: If the _TransactionWrapper is being used
+ as a context manager, it's when the __enter__ function is called; if it is being
+ used as a decorator, it's when the decorated function is about to be called
+ (see the `inner` function below in __call__).
+
+ The __enter__ and __exit__ functions on _TransactionWrapper are only called
+ if we're using xact() as a context manager; if we're using it as a decorator,
+ they're skipped and self.transaction is always None. Similarly, __call__ is
+ not used if this is a context manager usage. This is not super-elegant, but
+ it's the only way I've found to allow xact() to be used as both a context
+ manager and a decorator using the same syntax.
+ """
+
+ def __init__(self, using):
+ self.using = using
+ self.transaction = None
+ def __enter__(self):
+ if self.manager is None:

This should probably be:

if self.transaction is None:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ self.transaction = _Transaction(self.using)
+ return self.transaction.__enter__()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ return self.transaction.__exit__(exc_type, exc_value, traceback)
+
def __call__(self, func):
@wraps(func)
def inner(*args, **kwargs):
- with self:
+ with _Transaction(self.using):
return func(*args, **kwargs)
return inner
@@ -87,8 +125,22 @@ def xact(using=None):
if using is None:
using = DEFAULT_DB_ALIAS
if callable(using):
- return _Transaction(DEFAULT_DB_ALIAS)(using)
- return _Transaction(using)
+ # We end up here if xact is being used as a completely bare decorator:
+ # @xact
+ # (not even an empty parameter list)
+ return _TransactionWrapper(DEFAULT_DB_ALIAS)(using)
+ # Note that `using` here is *not* the database alias; it's the actual function
+ # being decorated.
+ else:
+ # We end up here if xact is being used as a parameterized decorator (including
+ # default parameter):
+ # @xact(db)
+ # or @xact()
+ # ... or as a context manager:
+ # with xact():
+ # ...
+ return _TransactionWrapper(using)
+
# -----------------------------------------------------------------------------

0 comments on commit 5beefd1

Please sign in to comment.