[Zope3-checkins] CVS: Zope3/src/zodb/tests - test_connection.py:1.3.4.1

Jeremy Hylton jeremy@zope.com
Sat, 1 Mar 2003 21:38:04 -0500


Update of /cvs-repository/Zope3/src/zodb/tests
In directory cvs.zope.org:/tmp/cvs-serv19868/src/zodb/tests

Modified Files:
      Tag: jeremy-atomic-invalidation-branch
	test_connection.py 
Log Message:
Add two tests of read conflicts.

One test is a minimal test that verifies a read conflict is raised
when needed.

The other is Shane's test that verifies that an application can't
catch a read conflict and then commit a bogus transaction.


=== Zope3/src/zodb/tests/test_connection.py 1.3 => 1.3.4.1 ===
--- Zope3/src/zodb/tests/test_connection.py:1.3	Tue Feb 25 13:55:04 2003
+++ Zope3/src/zodb/tests/test_connection.py	Sat Mar  1 21:38:00 2003
@@ -14,11 +14,14 @@
 import unittest
 
 from persistence import Persistent
+from persistence.dict import PersistentDict
 from transaction.tests.abstestIDataManager import IDataManagerTests
+from transaction import get_transaction
 
 from zodb.db import DB
 from zodb.storage.mapping import MappingStorage
 from zodb.ztransaction import Transaction
+from zodb.interfaces import ReadConflictError, ConflictError
 
 class P(Persistent):
     pass
@@ -31,6 +34,10 @@
         self.obj = P()
         self.txn_factory = Transaction
 
+    def tearDown(self):
+        # Make sure the test doesn't leave a transaction active.
+        get_transaction().abort()
+
     def get_transaction(self):
         t = super(ConnectionTests, self).get_transaction()
         t.setUser('IDataManagerTests')
@@ -39,6 +46,89 @@
 
     def test_cacheGC(self):
         self.datamgr.cacheGC()
+
+    def testReadConflict(self):
+        # 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.
+
+        r1 = self.datamgr.root()
+        r1["p"] = self.obj
+        self.obj.child1 = P()
+        get_transaction().commit()
+
+        # start a new transaction with a new connection
+        cn2 = self.db.open()
+        r2 = cn2.root()
+
+        # start a new transaction with the other connection
+        txn = get_transaction()
+        txn.suspend()
+
+        self.obj.child2 = P()
+        get_transaction().commit()
+
+        # resume the transaction using cn2
+        txn.resume()
+        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.
+        self.assertRaises(ReadConflictError, lambda: obj.child1)
+        txn.abort()
+
+    def testReadConflictIgnored(self):
+        # Test that an application that catches a read conflict and
+        # continues can not commit the transaction later.
+        root = self.datamgr.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()
+        r2 = cn2.root()
+        real_data2 = r2["real_data"]
+        index2 = r2["index"]
+
+        # start a new transaction with the other connection
+        txn = get_transaction()
+        txn.suspend()
+
+        real_data["b"]["indexed_value"] = False
+        del index[True]["b"]
+        index[False]["b"] = 1
+        get_transaction().commit()
+
+        # switch back to the other transaction
+        txn.resume()
+
+        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, txn.commit)
+        get_transaction().abort()
 
     def tearDown(self):
         self.datamgr.close()