[Checkins] SVN: z3c.zalchemy/trunk/ - Added a way to register database specific adapters for conflict detection and

Christian Zagrodnick cz at gocept.com
Mon Sep 24 08:00:34 EDT 2007


Log message for revision 79874:
  - Added a way to register database specific adapters for conflict detection and
  possible re-do by the publisher.  
  
  

Changed:
  U   z3c.zalchemy/trunk/CHANGES.txt
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py
  U   z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt

-=-
Modified: z3c.zalchemy/trunk/CHANGES.txt
===================================================================
--- z3c.zalchemy/trunk/CHANGES.txt	2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/CHANGES.txt	2007-09-24 12:00:34 UTC (rev 79874)
@@ -5,6 +5,9 @@
 0.2 - unreleased
 ================
 
+  - Added a way to register database specific adapters for conflict detection
+    and possible re-do by the publisher.
+
   - Using the threadlocal strategy of sqlalchemy instead of doing that
     ourselves.
 

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py	2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/__init__.py	2007-09-24 12:00:34 UTC (rev 79874)
@@ -12,16 +12,18 @@
 #
 ##############################################################################
 
-from datamanager import (
-        getSession,
-        inSession,
-        assignTable,
-        assignClass,
-        createTable,
-        metadata,
-        getEngineForTable,
-        )
+import zope.deferredimport
 
+zope.deferredimport.define(
+    getSession='z3c.zalchemy.datamanager:getSession',
+    inSession='z3c.zalchemy.datamanager:inSession',
+    assignTable='z3c.zalchemy.datamanager:assignTable',
+    assignClass='z3c.zalchemy.datamanager:assignClass',
+    createTable='z3c.zalchemy.datamanager:createTable',
+    metadata='z3c.zalchemy.datamanager:metadata',
+    getEngineForTable='z3c.zalchemy.datamanager:getEngineForTable')
+
+
 import zope.i18nmessageid
 
 _ = zope.i18nmessageid.MessageFactory('z3c.zalchemy')

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py	2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/datamanager.py	2007-09-24 12:00:34 UTC (rev 79874)
@@ -21,7 +21,7 @@
 from transaction.interfaces import IDataManager, ISynchronizer
 from transaction.interfaces import IDataManagerSavepoint
 
-from interfaces import IAlchemyEngineUtility
+import z3c.zalchemy.interfaces
 
 import sqlalchemy
 from sqlalchemy.orm.mapper import global_extensions
@@ -31,7 +31,7 @@
 class AlchemyEngineUtility(persistent.Persistent):
     """A utility providing a database engine.
     """
-    implements(IAlchemyEngineUtility)
+    implements(z3c.zalchemy.interfaces.IAlchemyEngineUtility)
 
     def __init__(self, name, dsn, echo=False, encoding='utf-8',
                  convert_unicode=False, **kwargs):
@@ -64,9 +64,9 @@
             self._v_engine = None
 
 
-for name in IAlchemyEngineUtility:
+for name in z3c.zalchemy.interfaces.IAlchemyEngineUtility:
     setattr(AlchemyEngineUtility, name, FieldProperty(
-        IAlchemyEngineUtility[name]))
+        z3c.zalchemy.interfaces.IAlchemyEngineUtility[name]))
 
 
 _tableToEngine = {}
@@ -81,7 +81,7 @@
     hooked up with the Zope transaction machinery.
 
     """
-    util = queryUtility(IAlchemyEngineUtility)
+    util = queryUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility)
     if util is None:
         raise ValueError("No engine utility registered")
     engine = util.getEngine()
@@ -113,7 +113,8 @@
 
 def getEngineForTable(t):
     name = _tableToEngine[t]
-    util = getUtility(IAlchemyEngineUtility, name=name)
+    util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+                      name=name)
     return util.getEngine()
 
 
@@ -152,7 +153,8 @@
 
 def _assignTable(table, engine, session=None):
     t = metadata.getTable(engine, table, True)
-    util = getUtility(IAlchemyEngineUtility, name=engine)
+    util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+                      name=engine)
     if session is None:
             session = ctx.current
     session.bind_table(t, util.getEngine())
@@ -160,7 +162,8 @@
 
 def _assignClass(class_, engine, session=None):
     m = sqlalchemy.orm.class_mapper(class_)
-    util = getUtility(IAlchemyEngineUtility, name=engine)
+    util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+                      name=engine)
     if session is None:
         session = ctx.current
     session.bind_mapper(m,util.getEngine())
@@ -174,7 +177,8 @@
 
 
 def _doCreateTable(table, engine):
-    util = getUtility(IAlchemyEngineUtility, name=engine)
+    util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+                      name=engine)
     t = metadata.getTable(engine, table, True)
     try:
         util.getEngine().create(t)
@@ -183,7 +187,8 @@
 
 
 def dropTable(table, engine=''):
-    util = getUtility(IAlchemyEngineUtility, name=engine)
+    util = getUtility(z3c.zalchemy.interfaces.IAlchemyEngineUtility,
+                      name=engine)
     t = metadata.getTable(engine, table, True)
     try:
         util.getEngine().drop(t)
@@ -208,7 +213,13 @@
         pass
 
     def commit(self, trans):
-        self.session.flush()
+        try:
+            self.session.flush()
+        except Exception, e:
+            conflict = z3c.zalchemy.interfaces.IConflictError(e, None)
+            if conflict is None:
+                raise
+            raise conflict
 
     def tpc_vote(self, trans):
         pass

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py	2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/interfaces.py	2007-09-24 12:00:34 UTC (rev 79874)
@@ -126,3 +126,12 @@
            usefull for using the engine to execute literal sql statements
         """
 
+
+class IConflictError(interface.Interface):
+    """Two transactions tried to modify the same object at once.
+
+    Exceptions occuring on commit/flush will be adapted to this interface.
+    Registered adapters must either return an ZODB.POSException.ConflictError
+    or None. A ConflictError will tell the publisher to retry the transaction.
+
+    """

Modified: z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt
===================================================================
--- z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt	2007-09-24 10:14:53 UTC (rev 79873)
+++ z3c.zalchemy/trunk/src/z3c/zalchemy/tests/TRANSACTION.txt	2007-09-24 12:00:34 UTC (rev 79874)
@@ -195,3 +195,80 @@
 
   >>> transaction.abort()
 
+
+Conflicts
+---------
+
+With the a serialisable isolation level it is possible to get conflicts with a
+relational database. For correct integration we need to convert such a conflict
+error to a ZODB.POSException.ConflictError.
+
+Since every database backend yields different exceptions an exception is
+adapted to IConflictError.
+
+Let's use a mock session here which issues a conflict error when asked to:
+
+>>> class MockSession(object):
+...
+...     conflict = 'Conflict'
+...
+...     def flush(self):
+...         if self.conflict:
+...             raise sqlalchemy.exceptions.SQLError(
+...                 'UPDATE bla...', (1, 2, 3), ValueError(self.conflict))
+...
+...     def create_transaction(self):
+...         pass
+...
+
+Now create a datamanager:
+
+>>> dm = z3c.zalchemy.datamanager.AlchemyDataManager(MockSession())
+
+When we don't do anything we'll get the exception as specified above:
+
+>>> dm.commit(None)
+Traceback (most recent call last):
+    ...
+SQLError: (ValueError) Conflict 'UPDATE bla...' (1, 2, 3)
+
+
+When we now provide an adapter from SQLError to IConflictError we'll get a
+ZODB ConflictError:
+
+>>> import ZODB.POSException
+>>> def adapt_sqlerror(context):
+...     if context.orig.args == ('Conflict', ):
+...         return ZODB.POSException.ConflictError('play it again')
+>>> import zope.component
+>>> gsm = zope.component.getGlobalSiteManager()
+>>> gsm.registerAdapter(adapt_sqlerror,
+...     (sqlalchemy.exceptions.SQLError, ),
+...      z3c.zalchemy.interfaces.IConflictError)
+
+
+So commit:
+
+>>> dm.commit(None)
+Traceback (most recent call last):
+    ...
+ConflictError: play it again
+
+
+Note, that we added a condition to the adapter, so we'll only get a conflict
+error when the argument is "Conflict". Otherise the original exception is
+raised:
+
+>>> MockSession.conflict = 'No conflict'
+>>> dm.commit(None)
+Traceback (most recent call last):
+    ...
+SQLError: (ValueError) No conflict 'UPDATE bla...' (1, 2, 3)
+    
+
+Clean up:
+
+>>> gsm.unregisterAdapter(adapt_sqlerror,
+...     (sqlalchemy.exceptions.SQLError, ),
+...      z3c.zalchemy.interfaces.IConflictError)
+True



More information about the Checkins mailing list