[Zodb-checkins] SVN: ZODB/trunk/src/ZODB/ Added new APIs to check to make sure data read is up to date when used

Jim Fulton jim at zope.com
Thu Sep 2 09:55:19 EDT 2010


Log message for revision 116128:
  Added new APIs to check to make sure data read is up to date when used
  to decide how to write other data.
  

Changed:
  U   ZODB/trunk/src/ZODB/Connection.py
  U   ZODB/trunk/src/ZODB/interfaces.py
  U   ZODB/trunk/src/ZODB/tests/testConnection.py

-=-
Modified: ZODB/trunk/src/ZODB/Connection.py
===================================================================
--- ZODB/trunk/src/ZODB/Connection.py	2010-09-02 06:39:25 UTC (rev 116127)
+++ ZODB/trunk/src/ZODB/Connection.py	2010-09-02 13:55:17 UTC (rev 116128)
@@ -134,6 +134,10 @@
         # of this list are either in _cache or in _added.
         self._registered_objects = []
 
+        # ids and serials of objects for which readCurrent was called
+        # in a transaction.
+        self._readCurrent = {}
+
         # Dict of oid->obj added explicitly through add(). Used as a
         # preliminary cache until commit time when objects are all moved
         # to the real _cache. The objects are moved to _creating at
@@ -552,6 +556,10 @@
         else:
             self._commit(transaction)
 
+        for oid, serial in self._readCurrent.iteritems():
+            self._storage.checkCurrentSerialInTransaction(
+                oid, serial, transaction)
+
     def _commit(self, transaction):
         """Commit changes to an object"""
 
@@ -674,6 +682,11 @@
             self._handle_serial(oid, s)
 
     def _handle_serial(self, oid, serial, change=True):
+
+        # if we write an object, we don't want to check if it was read
+        # while current.  This is a convenient choke point to do this.
+        self._readCurrent.pop(oid, None)
+
         if not serial:
             return
         if not isinstance(serial, str):
@@ -782,6 +795,7 @@
     # pending invalidations regardless.  Of course this should only be
     # called at transaction boundaries.
     def _storage_sync(self, *ignored):
+        self._readCurrent.clear()
         sync = getattr(self._storage, 'sync', 0)
         if sync:
             sync()
@@ -949,6 +963,10 @@
         if obj is not None:
             self._registered_objects.append(obj)
 
+    def readCurrent(self, ob):
+        assert ob._p_jar is self
+        assert ob._p_oid is not None and ob._p_serial is not None
+        self._readCurrent[ob._p_oid] = ob._p_serial
 
     # persistent.interfaces.IPersistentDatamanager
     ##########################################################################

Modified: ZODB/trunk/src/ZODB/interfaces.py
===================================================================
--- ZODB/trunk/src/ZODB/interfaces.py	2010-09-02 06:39:25 UTC (rev 116127)
+++ ZODB/trunk/src/ZODB/interfaces.py	2010-09-02 13:55:17 UTC (rev 116128)
@@ -286,7 +286,15 @@
         begins or until the connection os reopned.
         """
 
+    def readCurrent(obj):
+        """Make sure an object being read is current
 
+        This is used when applications want to ensure a higher level
+        of consistency for some operations. This should be called when
+        an object is read and the information read is used to write a
+        separate object.
+        """
+
 class IStorageWrapper(Interface):
     """Storage wrapper interface
 
@@ -1119,6 +1127,22 @@
        commit.
        """
 
+class ReadVerifyingStorage(IStorage):
+
+    def checkCurrentSerialInTransaction(oid, serial, transaction):
+        """Check whether the given serial number is current.
+
+        The method is called during the first phase of 2-phase commit
+        to verify that data read in a transaction is current.
+
+        The storage should raise a ReadConflictError if the serial is not
+        current, although it may raise the exception later, in a call
+        to store or in a call to tpc_vote.
+
+        If no exception is raised, then the serial must remain current
+        through the end of the transaction.
+        """
+
 class IBlob(Interface):
     """A BLOB supports efficient handling of large data within ZODB."""
 

Modified: ZODB/trunk/src/ZODB/tests/testConnection.py
===================================================================
--- ZODB/trunk/src/ZODB/tests/testConnection.py	2010-09-02 06:39:25 UTC (rev 116127)
+++ ZODB/trunk/src/ZODB/tests/testConnection.py	2010-09-02 13:55:17 UTC (rev 116128)
@@ -611,6 +611,150 @@
 
     """
 
+def readCurrent():
+    r"""
+The connection's readCurrent method is called to provide a higher
+level of consistency in cases where an object if read to compute an
+update to a separate object.  When this is used, the
+checkCurrentSerialInTransaction method on the storage is called in
+2-phase commit.
+
+To demonstrate this, we'll create a storage and give it a test
+implementation of checkCurrentSerialInTransaction.
+
+    >>> import ZODB.MappingStorage
+    >>> store = ZODB.MappingStorage.MappingStorage()
+
+    >>> from ZODB.POSException import ReadConflictError
+    >>> bad = set()
+    >>> def checkCurrentSerialInTransaction(oid, serial, trans):
+    ...     print 'checkCurrentSerialInTransaction', `oid`
+    ...     if not trans == transaction.get(): print 'oops'
+    ...     if oid in bad:
+    ...         raise ReadConflictError(oid=oid)
+
+    >>> store.checkCurrentSerialInTransaction = checkCurrentSerialInTransaction
+
+Now, we'll use the storage as usual.  checkCurrentSerialInTransaction
+won't normally be called:
+
+    >>> db = ZODB.DB(store)
+    >>> conn = db.open()
+    >>> conn.root.a = ZODB.tests.util.P('a')
+    >>> conn.root.b = ZODB.tests.util.P('b')
+    >>> transaction.commit()
+
+If we call readCurrent for an object and we modify another object,
+then checkCurrentSerialInTransaction will be called for the object
+readCurrent was called on.
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.root.b.x = 0
+    >>> transaction.commit()
+    checkCurrentSerialInTransaction '\x00\x00\x00\x00\x00\x00\x00\x01'
+
+It doesn't matter how often we call readCurrent,
+checkCurrentSerialInTransaction will be called only once:
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.root.b.x += 1
+    >>> transaction.commit()
+    checkCurrentSerialInTransaction '\x00\x00\x00\x00\x00\x00\x00\x01'
+
+checkCurrentSerialInTransaction won't be called if another object
+isn't modified:
+
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> transaction.commit()
+
+Or if the object it was called on is modified:
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.root.a.x = 0
+    >>> conn.root.b.x += 1
+    >>> transaction.commit()
+
+If the storage raises a conflict error, it'll be propigated:
+
+    >>> bad.add(conn.root.a._p_oid)
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.root.b.x += 1
+    >>> transaction.commit()
+    Traceback (most recent call last):
+    ...
+    ReadConflictError: database read conflict error (oid 0x01)
+
+    >>> transaction.abort()
+
+The storage may raise it later:
+
+    >>> def checkCurrentSerialInTransaction(oid, serial, trans):
+    ...     if not trans == transaction.get(): print 'oops'
+    ...     print 'checkCurrentSerialInTransaction', `oid`
+    ...     store.badness = ReadConflictError(oid=oid)
+
+    >>> def tpc_vote(t):
+    ...     if store.badness:
+    ...        badness = store.badness
+    ...        store.badness = None
+    ...        raise badness
+
+    >>> store.checkCurrentSerialInTransaction = checkCurrentSerialInTransaction
+    >>> store.badness = None
+    >>> store.tpc_vote = tpc_vote
+
+It will still be propigated:
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.root.b.x = +1
+    >>> transaction.commit()
+    Traceback (most recent call last):
+    ...
+    ReadConflictError: database read conflict error (oid 0x01)
+
+    >>> transaction.abort()
+
+Read checks don't leak accross transactions:
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> transaction.commit()
+    >>> conn.root.b.x = +1
+    >>> transaction.commit()
+
+Read checks to work accross savepoints.
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> conn.root.b.x = +1
+    >>> _ = transaction.savepoint()
+    >>> transaction.commit()
+    Traceback (most recent call last):
+    ...
+    ReadConflictError: database read conflict error (oid 0x01)
+
+    >>> transaction.abort()
+
+    >>> conn.readCurrent(conn.root.a)
+    >>> _ = transaction.savepoint()
+    >>> conn.root.b.x = +1
+    >>> transaction.commit()
+    Traceback (most recent call last):
+    ...
+    ReadConflictError: database read conflict error (oid 0x01)
+
+    >>> transaction.abort()
+
+    """
+
+# check interaction w savepoint
+# check call in read-only trans followed by write trans
+
+
+
+
 class _PlayPersistent(Persistent):
     def setValueWithSize(self, size=0): self.value = size*' '
     __init__ = setValueWithSize



More information about the Zodb-checkins mailing list