[Zope-Checkins] CVS: ZODB3/ZODB/tests - testZODB.py:1.10

Jeremy Hylton jeremy@zope.com
Tue, 8 Apr 2003 11:55:45 -0400


Update of /cvs-repository/ZODB3/ZODB/tests
In directory cvs.zope.org:/tmp/cvs-serv13071/ZODB/tests

Modified Files:
	testZODB.py 
Log Message:
Backport atomic invalidations code from Zope3.

The DB's invalidate() method takes a set of oids corresponding to all
the changes from a data manager for one transaction.  All the objects
are invalidated at once.

Add a few tests in testZODB of the new code.  The tests just cover
corner cases, because I can't think of a sensible way to test the
atomicity.  When it has failed in the past, it's been caused by
nearly-impossible to reproduce data races.

This fix needs to be backported to Zope 2.6, but only after assessing
how significant an impact the API change will have.


=== ZODB3/ZODB/tests/testZODB.py 1.9 => 1.10 ===
--- ZODB3/ZODB/tests/testZODB.py:1.9	Fri Jan 17 12:23:16 2003
+++ ZODB3/ZODB/tests/testZODB.py	Tue Apr  8 11:55:45 2003
@@ -11,16 +11,52 @@
 # FOR A PARTICULAR PURPOSE.
 # 
 ##############################################################################
-import sys, os
+import unittest
 
 import ZODB
 import ZODB.FileStorage
 from ZODB.PersistentMapping import PersistentMapping
+from ZODB.POSException import ReadConflictError
 from ZODB.tests.StorageTestBase import removefs
-import unittest
+from Persistence import Persistent
+
+class P(Persistent):
+    pass
+
+class Independent(Persistent):
+
+    def _p_independent(self):
+        return True
+
+class DecoyIndependent(Persistent):
+
+    def _p_independent(self):
+        return False
+
+class ZODBTests(unittest.TestCase):
+
+    def setUp(self):
+        self._storage = ZODB.FileStorage.FileStorage(
+            'ZODBTests.fs', create=1)
+        self._db = ZODB.DB(self._storage)
+
+    def populate(self):
+        get_transaction().begin()
+        conn = self._db.open()
+        root = conn.root()
+        root['test'] = pm = PersistentMapping()
+        for n in range(100):
+            pm[n] = PersistentMapping({0: 100 - n})
+        get_transaction().note('created test data')
+        get_transaction().commit()
+        conn.close()
 
-class ExportImportTests:
-    def checkDuplicate(self, abort_it=0, dup_name='test_duplicate'):
+    def tearDown(self):
+        self._storage.close()
+        removefs("ZODBTests.fs")
+
+    def checkExportImport(self, abort_it=0, dup_name='test_duplicate'):
+        self.populate()
         get_transaction().begin()
         get_transaction().note('duplication')
         # Duplicate the 'test' object.
@@ -83,29 +119,8 @@
         finally:
             conn.close()
 
-    def checkDuplicateAborted(self):
-        self.checkDuplicate(abort_it=1, dup_name='test_duplicate_aborted')
-
-
-class ZODBTests(unittest.TestCase, ExportImportTests):
-
-    def setUp(self):
-        self._storage = ZODB.FileStorage.FileStorage(
-            'ZODBTests.fs', create=1)
-        self._db = ZODB.DB(self._storage)
-        get_transaction().begin()
-        conn = self._db.open()
-        root = conn.root()
-        root['test'] = pm = PersistentMapping()
-        for n in range(100):
-            pm[n] = PersistentMapping({0: 100 - n})
-        get_transaction().note('created test data')
-        get_transaction().commit()
-        conn.close()
-
-    def tearDown(self):
-        self._storage.close()
-        removefs("ZODBTests.fs")
+    def checkExportImportAborted(self):
+        self.checkExportImport(abort_it=1, dup_name='test_duplicate_aborted')
 
     def checkVersionOnly(self):
         # Make sure the changes to make empty transactions a no-op
@@ -124,6 +139,7 @@
     def checkResetCache(self):
         # The cache size after a reset should be 0 and the GC attributes
         # ought to be linked to it rather than the old cache.
+        self.populate()
         conn = self._db.open()
         try:
             conn.root()
@@ -173,10 +189,99 @@
             conn1.close()
             conn2.close()
 
+    def checkReadConflict(self):
+        self.obj = P()
+        self.readConflict()
+
+    def readConflict(self, shouldFail=True):
+        # Two transactions run concurrently.  Each reads some object,
+        # then one commits and the other tries to read an object
+        # modified by the first.  This read should fail with a conflict
+        # error because the object state read is not necessarily
+        # consistent with the objects read earlier in the transaction.
 
-def test_suite():
-    return unittest.makeSuite(ZODBTests, 'check')
+        conn = self._db.open()
+        conn.setLocalTransaction()
+        r1 = conn.root()
+        r1["p"] = self.obj
+        self.obj.child1 = P()
+        conn.getTransaction().commit()
+
+        # start a new transaction with a new connection
+        cn2 = self._db.open()
+        # start a new transaction with the other connection
+        cn2.setLocalTransaction()
+        r2 = cn2.root()
+
+        self.assertEqual(r1._p_serial, r2._p_serial)
+        
+        self.obj.child2 = P()
+        conn.getTransaction().commit()
+
+        # resume the transaction using cn2
+        obj = r2["p"]
+        # An attempt to access obj should fail, because r2 was read
+        # earlier in the transaction and obj was modified by the othe
+        # transaction.
+        if shouldFail:
+            self.assertRaises(ReadConflictError, lambda: obj.child1)
+        else:
+            # make sure that accessing the object succeeds
+            obj.child1
+        cn2.getTransaction().abort()
+
+    def testReadConflictIgnored(self):
+        # Test that an application that catches a read conflict and
+        # continues can not commit the transaction later.
+        root = self._db.open().root()
+        root["real_data"] = real_data = PersistentDict()
+        root["index"] = index = PersistentDict()
+
+        real_data["a"] = PersistentDict({"indexed_value": False})
+        real_data["b"] = PersistentDict({"indexed_value": True})
+        index[True] = PersistentDict({"b": 1})
+        index[False] = PersistentDict({"a": 1})
+        get_transaction().commit()
+
+        # load some objects from one connection
+        cn2 = self._db.open()
+        cn2.setLocalTransaction()
+        r2 = cn2.root()
+        real_data2 = r2["real_data"]
+        index2 = r2["index"]
+
+        real_data["b"]["indexed_value"] = False
+        del index[True]["b"]
+        index[False]["b"] = 1
+        cn2.getTransaction().commit()
 
-if __name__=='__main__':
-    unittest.main(defaultTest='test_suite')
+        del real_data2["a"]
+        try:
+            del index2[False]["a"]
+        except ReadConflictError:
+            # This is the crux of the text.  Ignore the error.
+            pass
+        else:
+            self.fail("No conflict occurred")
+
+        # real_data2 still ready to commit
+        self.assert_(real_data2._p_changed)
+
+        # index2 values not ready to commit
+        self.assert_(not index2._p_changed)
+        self.assert_(not index2[False]._p_changed)
+        self.assert_(not index2[True]._p_changed)
+
+        self.assertRaises(ConflictError, get_transaction().commit)
+        get_transaction().abort()
+
+    def checkIndependent(self):
+        self.obj = Independent()
+        self.readConflict(shouldFail=False)
+
+    def checkNotIndependent(self):
+        self.obj = DecoyIndependent()
+        self.readConflict()
 
+def test_suite():
+    return unittest.makeSuite(ZODBTests, 'check')