[Zodb-checkins] SVN: ZODB/branches/shane-cross-database-seatbelt/src/ZODB/ The proposed cross-database reference seat belt mechanism, with tests.

Shane Hathaway shane at hathawaymix.org
Tue Apr 28 03:37:42 EDT 2009


Log message for revision 99551:
  The proposed cross-database reference seat belt mechanism, with tests.
  

Changed:
  U   ZODB/branches/shane-cross-database-seatbelt/src/ZODB/DB.py
  U   ZODB/branches/shane-cross-database-seatbelt/src/ZODB/cross-database-references.txt
  U   ZODB/branches/shane-cross-database-seatbelt/src/ZODB/serialize.py

-=-
Modified: ZODB/branches/shane-cross-database-seatbelt/src/ZODB/DB.py
===================================================================
--- ZODB/branches/shane-cross-database-seatbelt/src/ZODB/DB.py	2009-04-28 07:31:40 UTC (rev 99550)
+++ ZODB/branches/shane-cross-database-seatbelt/src/ZODB/DB.py	2009-04-28 07:37:42 UTC (rev 99551)
@@ -399,6 +399,7 @@
                  historical_cache_size=1000,
                  historical_cache_size_bytes=0,
                  historical_timeout=300,
+                 check_xrefs=False,
                  database_name='unnamed',
                  databases=None,
                  ):
@@ -419,6 +420,10 @@
             the historical connection.
           - `historical_timeout`: minimum number of seconds that
             an unused historical connection will be kept, or None.
+          - `check_xrefs`: if true, cross-database references will only
+            be allowed from objects whose _p_check_xref method allows
+            them. If false (the default), cross-database references
+            will be allowed implicitly.
         """
         if isinstance(storage, basestring):
             from ZODB import FileStorage
@@ -437,6 +442,7 @@
         self._cache_size_bytes = cache_size_bytes
         self._historical_cache_size = historical_cache_size
         self._historical_cache_size_bytes = historical_cache_size_bytes
+        self.check_xrefs = check_xrefs
 
         # Setup storage
         self.storage = storage

Modified: ZODB/branches/shane-cross-database-seatbelt/src/ZODB/cross-database-references.txt
===================================================================
--- ZODB/branches/shane-cross-database-seatbelt/src/ZODB/cross-database-references.txt	2009-04-28 07:31:40 UTC (rev 99550)
+++ ZODB/branches/shane-cross-database-seatbelt/src/ZODB/cross-database-references.txt	2009-04-28 07:37:42 UTC (rev 99551)
@@ -133,6 +133,86 @@
 This the most explicit and thus the best way, when practical, to avoid
 the ambiguity.
 
+Cross-database reference seat belt
+----------------------------------
+
+Some applications create unintentional cross-database references when
+they move objects between containers stored in ZODB. For example, an
+object might be created in a session container located in a volatile
+database, then later moved (rather than copied) to the main database.
+In that case, the reference will break when the session expires or the
+server restarts.
+
+ZODB provides an optional seat belt that prevents unintentional
+cross-database references. The seat belt is disabled by default, but
+can be enabled by passing "check_xrefs=True" to the DB constructor.
+
+When the seat belt is enabled, every cross-database reference is
+checked by the _p_check_xref method of the persistent object holding
+the reference; objects that do not have a _p_check_xref method are
+not allowed to hold any cross-database references.
+
+Lets set up a multi-database with 2 databases, this time checking
+cross-database references from `main_db` but not from `session_db`:
+
+    >>> import ZODB.tests.util, transaction, persistent
+    >>> databases = {}
+    >>> main_db = ZODB.tests.util.DB(databases=databases, database_name='main',
+    ...     check_xrefs=True)
+    >>> session_db = ZODB.tests.util.DB(databases=databases,
+    ...     database_name='sessions')
+
+Create a persistent object in both databases:
+
+    >>> tm = transaction.TransactionManager()
+    >>> main_conn = main_db.open(transaction_manager=tm)
+    >>> main_obj = MyClass()
+    >>> main_conn.root()['p'] = main_obj
+    >>> session_conn = main_conn.get_connection('sessions')
+    >>> session_obj = MyClass()
+    >>> session_conn.root()['p'] = session_obj
+    >>> tm.commit()
+
+Try to create a reference from `main_obj` to `session_obj`, which is not
+allowed because the seat belt is enabled in `main_db` and `main_obj` has
+no _p_check_xref method:
+
+    >>> main_obj.someattr = session_obj
+    >>> tm.commit() # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    InvalidObjectReference: Attempt to store a cross database reference from an object that does not have a _p_check_xref method
+    >>> tm.abort()
+    >>> hasattr(main_obj, 'someattr')
+    False
+
+A reference from `session_obj` to `main_obj` is allowed, however.
+
+    >>> session_obj.someattr = main_obj
+    >>> tm.commit()
+    >>> hasattr(session_obj, 'someattr')
+    True
+
+The reference from `main_obj` can be allowed by its _p_check_xref method:
+
+    >>> def _p_check_xref(obj):
+    ...     return obj is session_obj
+    >>> main_obj._p_check_xref = _p_check_xref
+    >>> main_obj.someattr = session_obj
+    >>> tm.commit()
+
+The _p_check_xref method can also disallow a reference:
+
+    >>> def _p_check_xref(obj):
+    ...     return False
+    >>> main_obj._p_check_xref = _p_check_xref
+    >>> main_obj.someattr = session_obj
+    >>> tm.commit() # doctest: +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    InvalidObjectReference: A cross database reference was disallowed by a _p_check_xref method
+    >>> tm.abort()
+
 NOTE
 ----
 

Modified: ZODB/branches/shane-cross-database-seatbelt/src/ZODB/serialize.py
===================================================================
--- ZODB/branches/shane-cross-database-seatbelt/src/ZODB/serialize.py	2009-04-28 07:31:40 UTC (rev 99550)
+++ ZODB/branches/shane-cross-database-seatbelt/src/ZODB/serialize.py	2009-04-28 07:37:42 UTC (rev 99551)
@@ -175,6 +175,7 @@
         self._p = cPickle.Pickler(self._file, 1)
         self._p.inst_persistent_id = self.persistent_id
         self._stack = []
+        self._obj = obj
         if obj is not None:
             self._stack.append(obj)
             jar = obj._p_jar
@@ -352,8 +353,19 @@
                     "A new object is reachable from multiple databases. "
                     "Won't try to guess which one was correct!"
                     )
-                
 
+            if self._jar.db().check_xrefs:
+                method = getattr(self._obj, '_p_check_xref', None)
+                if method is None:
+                    raise InvalidObjectReference(
+                        "Attempt to store a cross database reference "
+                        "from an object that does not have a _p_check_xref "
+                        "method")
+                elif not method(obj):
+                    raise InvalidObjectReference(
+                        "A cross database reference was disallowed "
+                        "by a _p_check_xref method")
+
         klass = type(obj)
         if hasattr(klass, '__getnewargs__'):
             # We don't want to save newargs in object refs.



More information about the Zodb-checkins mailing list