[Zodb-checkins] SVN: ZODB/trunk/src/ZODB/ Because of the, um, automatic way that objects are added to databases

Jim Fulton jim at zope.com
Thu Jun 16 12:19:33 EDT 2005


Log message for revision 30811:
  Because of the, um, automatic way that objects are added to databases
  by virtue of being reachable from objects in databases, there is
  a danger of ambiguity when an object is reachable from multiple
  databases.  This ambiguity can lead to very subtle bugs with the
  symtom that objects are in unexpected databases.
  
  Introduced a check to refuse to commit when the ambiguity exists.
  
  (This required changing the connection '_creating' attribute to a
  dictionary.)
  

Changed:
  U   ZODB/trunk/src/ZODB/Connection.py
  U   ZODB/trunk/src/ZODB/cross-database-references.txt
  U   ZODB/trunk/src/ZODB/serialize.py
  U   ZODB/trunk/src/ZODB/tests/testcrossdatabasereferences.py

-=-
Modified: ZODB/trunk/src/ZODB/Connection.py
===================================================================
--- ZODB/trunk/src/ZODB/Connection.py	2005-06-16 10:56:55 UTC (rev 30810)
+++ ZODB/trunk/src/ZODB/Connection.py	2005-06-16 16:19:33 UTC (rev 30811)
@@ -107,6 +107,7 @@
         self._reset_counter = global_reset_counter
         self._load_count = 0   # Number of objects unghosted
         self._store_count = 0  # Number of objects stored
+        self._creating = {}
 
         # List of oids of modified objects (to be invalidated on an abort).
         self._modified = []
@@ -395,6 +396,7 @@
             self._flush_invalidations()
         self._needs_to_join = True
         self._registered_objects = []
+        self._creating.clear()
 
     # Process pending invalidations.
     def _flush_invalidations(self):
@@ -445,7 +447,7 @@
 
         # _creating is a list of oids of new objects, which is used to
         # remove them from the cache if a transaction aborts.
-        self._creating = []
+        self._creating.clear()
         self._normal_storage.tpc_begin(transaction)
 
     def commit(self, transaction):
@@ -468,8 +470,9 @@
         """Commit changes to an object"""
 
         if self._import:
-            # TODO:  This code seems important for Zope, but needs docs
-            # to explain why.
+            # We are importing an export file. We alsways do this
+            # while making a savepoint so we can copy export data
+            # directly to out storage, typically a TmpStore.
             self._importDuringCommit(transaction, *self._import)
             self._import = None
 
@@ -516,7 +519,7 @@
 
             if serial == z64:
                 # obj is a new object
-                self._creating.append(oid)
+                self._creating[oid] = 1
                 # Because obj was added, it is now in _creating, so it can
                 # be removed from _added.
                 self._added.pop(oid, None)
@@ -622,7 +625,7 @@
         """Disown any objects newly saved in an uncommitted transaction."""
         if creating is None:
             creating = self._creating
-            self._creating = []
+            self._creating = {}
 
         for oid in creating:
             o = self._cache.get(oid)
@@ -1033,10 +1036,10 @@
                                                self._normal_storage)
             self._storage = self._savepoint_storage
 
-        self._creating = []
+        self._creating.clear()
         self._commit(None)
-        self._storage.creating.extend(self._creating)
-        del self._creating[:]
+        self._storage.creating.update(self._creating)
+        self._creating.clear()
         self._registered_objects = []
 
         state = self._storage.position, self._storage.index.copy()
@@ -1061,7 +1064,7 @@
 
         # Copy invalidating and creating info from temporary storage:
         self._modified.extend(oids)
-        self._creating.extend(src.creating)
+        self._creating.update(src.creating)
 
         for oid in oids:
             data, serial = src.load(oid, src)
@@ -1129,7 +1132,7 @@
         self.position = 0L
         # index: map oid to pos of last committed version
         self.index = {}
-        self.creating = []
+        self.creating = {}
 
     def __len__(self):
         return len(self.index)

Modified: ZODB/trunk/src/ZODB/cross-database-references.txt
===================================================================
--- ZODB/trunk/src/ZODB/cross-database-references.txt	2005-06-16 10:56:55 UTC (rev 30810)
+++ ZODB/trunk/src/ZODB/cross-database-references.txt	2005-06-16 16:19:33 UTC (rev 30811)
@@ -63,6 +63,47 @@
     InvalidObjectReference:
     Attempt to store an object from a foreign database connection
 
+    >>> tm.abort()
+
+Databases for new objects
+-------------------------
+
+Objects are normally added to a database by making them reachable from
+an object already in the database.  This is unambiguous when there is
+only one database.  With modultiple databases, it is not so clear what
+happens.  Consider:
+
+    >>> p4 = MyClass()
+    >>> p1.p4 = p4
+    >>> p2.p4 = p4
+
+In this example, the new object is reachable from both p1 in database
+1 and p2 in database 2.  If we commit, which database will p4 end up
+in?  This sort of ambiguity can lead to subtle bugs. For that reason,
+an error is generated if we commit changes when new objects are
+reachable from multiple databases:
+
+    >>> tm.commit() # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    InvalidObjectReference: A new object is reachable from multiple
+    databases. Won't try to guess which one was correct!
+
+    >>> tm.abort()
+
+To resolve this ambiguity, we need to commit, or create a savepoint
+before an object becomes reachable from multiple databases.  Here
+we'll use a savepoint to make sure that p4 lands in database 1:
+
+    >>> p4 = MyClass()
+    >>> p1.p4 = p4
+    >>> s = tm.savepoint()
+    >>> p2.p4 = p4
+
+The advantage of using a savepoint is that we aren't making a
+commitment.  Changes made in the savepoint will be rolled back if the
+transaction is aborted.
+
 NOTE
 ----
 

Modified: ZODB/trunk/src/ZODB/serialize.py
===================================================================
--- ZODB/trunk/src/ZODB/serialize.py	2005-06-16 10:56:55 UTC (rev 30810)
+++ ZODB/trunk/src/ZODB/serialize.py	2005-06-16 16:19:33 UTC (rev 30811)
@@ -343,6 +343,16 @@
                     "a separate onnection to the same database or "
                     "multidatabase"
                     )
+
+            # OK, we have an object from another database.
+            # Lets make sure the object ws not *just* loaded.
+
+            # TODO: shouldn't depend on underware (_creating)
+            if oid in obj._p_jar._creating:
+                raise InvalidObjectReference(
+                    "A new object is reachable from multiple databases. "
+                    "Won't try to guess which one was correct!"
+                    )
                 
 
         klass = type(obj)
@@ -358,6 +368,7 @@
 
             if database_name:
                 return ['n', (database_name, oid)]
+
             return oid
 
         # Note that we never get here for persistent classes.
@@ -365,6 +376,7 @@
 
         if database_name:
             return ['m', (database_name, oid, klass)]
+
         return oid, klass
 
     def serialize(self, obj):

Modified: ZODB/trunk/src/ZODB/tests/testcrossdatabasereferences.py
===================================================================
--- ZODB/trunk/src/ZODB/tests/testcrossdatabasereferences.py	2005-06-16 10:56:55 UTC (rev 30810)
+++ ZODB/trunk/src/ZODB/tests/testcrossdatabasereferences.py	2005-06-16 16:19:33 UTC (rev 30811)
@@ -78,14 +78,19 @@
 
 """
 
+def tearDownDbs(test):
+    test.globs['db1'].close()
+    test.globs['db2'].close()
 
 def test_suite():
     return unittest.TestSuite((
         doctest.DocFileSuite('../cross-database-references.txt',
                              globs=dict(MyClass=MyClass),
+                             tearDown=tearDownDbs,
                              ),
         doctest.DocFileSuite('../cross-database-references.txt',
                              globs=dict(MyClass=MyClass_w_getnewargs),
+                             tearDown=tearDownDbs,
                              ),
         doctest.DocTestSuite(),
         ))



More information about the Zodb-checkins mailing list