[Zodb-checkins] SVN: ZODB/trunk/src/ZODB/ Replaced openDetatched method with a committed method that returns a

Jim Fulton jim at zope.com
Sat Jun 9 09:05:43 EDT 2007


Log message for revision 76546:
  Replaced openDetatched method with a committed method that returns a
  committed file name.
  
  Make sure that uncommitted data gets cleaned up if w blob is GCed
  without committing.
  

Changed:
  U   ZODB/trunk/src/ZODB/Connection.py
  U   ZODB/trunk/src/ZODB/blob.py
  U   ZODB/trunk/src/ZODB/interfaces.py
  U   ZODB/trunk/src/ZODB/tests/blob_transaction.txt
  U   ZODB/trunk/src/ZODB/tests/testblob.py

-=-
Modified: ZODB/trunk/src/ZODB/Connection.py
===================================================================
--- ZODB/trunk/src/ZODB/Connection.py	2007-06-09 12:12:05 UTC (rev 76545)
+++ ZODB/trunk/src/ZODB/Connection.py	2007-06-09 13:05:41 UTC (rev 76546)
@@ -38,6 +38,7 @@
 
 import transaction
 
+from ZODB.blob import SAVEPOINT_SUFFIX
 from ZODB.ConflictResolution import ResolvedSerial
 from ZODB.ExportImport import ExportImport
 from ZODB import POSException
@@ -616,7 +617,7 @@
                 if obj.opened():
                     raise ValueError("Can't commit with opened blobs.")
                 s = self._storage.storeBlob(oid, serial, p,
-                                            obj._p_blob_uncommitted,
+                                            obj._uncommitted(),
                                             self._version, transaction)
                 # we invalidate the object here in order to ensure
                 # that that the next attribute access of its name
@@ -1170,9 +1171,6 @@
     def rollback(self):
         self.datamanager._rollback(self.state)
 
-BLOB_SUFFIX = ".blob"
-BLOB_DIRTY = "store"
-
 class TmpStore:
     """A storage-like thing to support savepoints."""
 
@@ -1271,8 +1269,7 @@
 
     def _getCleanFilename(self, oid, tid):
         return os.path.join(self._getBlobPath(oid),
-                            "%s%s" % (utils.tid_repr(tid), 
-                                      BLOB_SUFFIX,)
+                            "%s%s" % (utils.tid_repr(tid), SAVEPOINT_SUFFIX,)
                             )
 
     def temporaryDirectory(self):

Modified: ZODB/trunk/src/ZODB/blob.py
===================================================================
--- ZODB/trunk/src/ZODB/blob.py	2007-06-09 12:12:05 UTC (rev 76545)
+++ ZODB/trunk/src/ZODB/blob.py	2007-06-09 13:05:41 UTC (rev 76546)
@@ -40,6 +40,7 @@
 logger = logging.getLogger('ZODB.blob')
 
 BLOB_SUFFIX = ".blob"
+SAVEPOINT_SUFFIX = ".spb"
 
 valid_modes = 'r', 'w', 'r+', 'a'
 
@@ -85,9 +86,7 @@
             if f is not None:
                 f.close()
 
-        if (self._p_blob_uncommitted
-            and os.path.exists(self._p_blob_uncommitted)
-            ):
+        if (self._p_blob_uncommitted):
             os.remove(self._p_blob_uncommitted)
 
         super(Blob, self)._p_invalidate()
@@ -159,6 +158,16 @@
 
         return result
 
+    def committed(self):
+        if (self._p_blob_uncommitted
+            or
+            not self._p_blob_committed
+            or
+            self._p_blob_committed.endswith(SAVEPOINT_SUFFIX)
+            ):
+            raise BlobError('Uncommitted changes')
+        return self._p_blob_committed
+
     def openDetached(self, class_=file):
         """Returns a file(-like) object in read mode that can be used
         outside of transaction boundaries.
@@ -234,9 +243,23 @@
             tempdir = self._p_jar.db()._storage.temporaryDirectory()
         else:
             tempdir = tempfile.gettempdir()
-        self._p_blob_uncommitted = utils.mktemp(dir=tempdir)
-        return self._p_blob_uncommitted
+        filename = utils.mktemp(dir=tempdir)
+        self._p_blob_uncommitted = filename
 
+        def cleanup(ref):
+            if os.path.exists(filename):
+                os.remove(filename)
+        
+        self._p_blob_ref = weakref.ref(self, cleanup)
+        return filename
+
+    def _uncommitted(self):
+        # hand uncommitted data to connection, relinquishing responsibility
+        # for it.
+        filename = self._p_blob_uncommitted
+        self._p_blob_uncommitted = self._p_blob_ref = None
+        return filename
+
 class BlobFile(file):
     """A BlobFile that holds a file handle to actual blob data.
 

Modified: ZODB/trunk/src/ZODB/interfaces.py
===================================================================
--- ZODB/trunk/src/ZODB/interfaces.py	2007-06-09 12:12:05 UTC (rev 76545)
+++ ZODB/trunk/src/ZODB/interfaces.py	2007-06-09 13:05:41 UTC (rev 76546)
@@ -910,6 +910,18 @@
         mode: Mode to open the file with. Possible values: r,w,r+,a
         """
 
+    def committed():
+        """Return a file name for committed data.
+
+        The returned file name may be opened for reading or handed to
+        other processes for reading.  The file name isn't guarenteed
+        to be valid indefinately.  The file may be removed in the
+        future as a result of garbage collection depending on system
+        configuration.
+
+        A BlobError will be raised if the blob has any uncommitted data.
+        """
+
     def consumeFile(filename):
         """Consume a file.
 

Modified: ZODB/trunk/src/ZODB/tests/blob_transaction.txt
===================================================================
--- ZODB/trunk/src/ZODB/tests/blob_transaction.txt	2007-06-09 12:12:05 UTC (rev 76545)
+++ ZODB/trunk/src/ZODB/tests/blob_transaction.txt	2007-06-09 13:05:41 UTC (rev 76546)
@@ -263,7 +263,8 @@
 --------------------------------------
 
 If you want to read from a Blob outside of transaction boundaries (e.g. to
-stream a file to the browser), you can use the openDetached() method::
+stream a file to the browser), committed method to get the name of a
+file that can be opened.
 
     >>> connection6 = database.open()
     >>> root6 = connection6.root()
@@ -273,60 +274,38 @@
     >>> blob_fh.close()
     >>> root6['blob'] = blob
     >>> transaction.commit()
-    >>> blob.openDetached().read()
+    >>> open(blob.committed()).read()
     "I'm a happy blob."
 
-Of course, that doesn't work for empty blobs::
+An exception is raised if we call committed on a blob that has
+uncommitted changes:
 
     >>> blob = Blob()
-    >>> blob.openDetached()
+    >>> blob.committed()
     Traceback (most recent call last):
-        ...
-    BlobError: Blob does not exist.
+    ...
+    BlobError: Uncommitted changes
 
-nor when the Blob is already opened for writing::
+    >>> blob.open('w').write("I'm a happy blob.")
+    >>> root6['blob6'] = blob
+    >>> blob.committed()
+    Traceback (most recent call last):
+    ...
+    BlobError: Uncommitted changes
 
-    >>> blob = Blob()
-    >>> blob_fh = blob.open("w")
-    >>> blob.openDetached()
+    >>> s = transaction.savepoint()
+    >>> blob.committed()
     Traceback (most recent call last):
-        ...
-    BlobError: Already opened for writing.
+    ...
+    BlobError: Uncommitted changes
 
-You can also pass a factory to the openDetached method that will be used to
-instantiate the file. This is used for e.g. creating filestream iterators::
-
-    >>> class customfile(file):
-    ...   pass
-    >>> blob_fh.write('Something')
-    >>> blob_fh.close()
-    >>> fh = blob.openDetached(customfile)
-    >>> fh  # doctest: +ELLIPSIS
-    <open file '...', mode 'rb' at 0x...>
-    >>> isinstance(fh, customfile)
-    True
-
-
-Note: Nasty people could use a factory that opens the file for writing. This
-would be evil.
-
-It does work when the transaction was aborted, though::
-
-    >>> blob = Blob()
-    >>> blob_fh = blob.open("w")
-    >>> blob_fh.write("I'm a happy blob.")
-    >>> blob_fh.close()
-    >>> root6['blob'] = blob
     >>> transaction.commit()
-
-    >>> blob_fh = blob.open("w")
-    >>> blob_fh.write("And I'm singing.")
-    >>> blob_fh.close()
-    >>> transaction.abort()
-    >>> blob.openDetached().read()
+    >>> open(blob.committed()).read()
     "I'm a happy blob."
 
 
+
+
 Teardown
 --------
 

Modified: ZODB/trunk/src/ZODB/tests/testblob.py
===================================================================
--- ZODB/trunk/src/ZODB/tests/testblob.py	2007-06-09 12:12:05 UTC (rev 76545)
+++ ZODB/trunk/src/ZODB/tests/testblob.py	2007-06-09 13:05:41 UTC (rev 76546)
@@ -265,6 +265,19 @@
 
         database.close()
 
+def gc_blob_removes_uncommitted_data():
+    """
+    >>> from ZODB.blob import Blob
+    >>> blob = Blob()
+    >>> blob.open('w').write('x')
+    >>> fname = blob._p_blob_uncommitted
+    >>> os.path.exists(fname)
+    True
+    >>> blob = None
+    >>> os.path.exists(fname)
+    False
+    """
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(ZODBBlobConfigTest))
@@ -275,12 +288,13 @@
         setUp=ZODB.tests.util.setUp,
         tearDown=ZODB.tests.util.tearDown,
         ))
+    suite.addTest(doctest.DocTestSuite(
+        setUp=ZODB.tests.util.setUp,
+        tearDown=ZODB.tests.util.tearDown,
+        ))
     suite.addTest(unittest.makeSuite(BlobUndoTests))
 
     return suite
 
 if __name__ == '__main__':
     unittest.main(defaultTest = 'test_suite')
-
-
-



More information about the Zodb-checkins mailing list