[Zope-Checkins] CVS: StandaloneZODB/ZODB - BaseStorage.py:1.18 FileStorage.py:1.79 POSException.py:1.10 fsdump.py:1.2

Guido van Rossum guido@python.org
Thu, 24 Jan 2002 21:15:08 -0500


Update of /cvs-repository/StandaloneZODB/ZODB
In directory cvs.zope.org:/tmp/cvs-serv21356

Modified Files:
	BaseStorage.py FileStorage.py POSException.py fsdump.py 
Log Message:
Commit changes from the short-lived "Recovery" branch to the trunk.
Highlights:

BaseStorage.py:

Fix copyTransactionsFrom() when commitVersion(), abortVersion() or
transactionalUndo() is used.

FileStorage.py:

Add restore() method, which can store data records corresponding to
undo or version manipulations; add close() method to FileIterator
class; raise POSKeyError instead of KeyError for bad keys, to ensure
safe formatting of transaction ids (binary strings) in tracebacks.

POSException.py:

Add POSKeyError.

fsdump.py:

Deal with records indicating the undo or abort of a version doing
object creation.

tests/IteratorStorage.py:

New unittests for the iterator() method and interface of the
storage API; new unit tests for extended file iterators; new class,
IteratorDeepCompare; test of the iterator .close() method.

tests/testFileStorage.py:

Add class FileStorageRecoveryTest, which adds two simple tests for
copyTransactionsFrom().  This indirectly tests the new restore()
method.



=== StandaloneZODB/ZODB/BaseStorage.py 1.17 => 1.18 ===
         _ts=None
         ok=1
-        preindex={}; preget=preindex.get   # waaaa
+        preindex={};
+        preget=preindex.get   # waaaa
+        # restore() is a new storage API method which has an identical
+        # signature to store() except that it does not return anything.
+        # Semantically, restore() is also identical to store() except that it
+        # doesn't do the ConflictError or VersionLockError consistency
+        # checks.  The reason to use restore() over store() in this method is
+        # that store() cannot be used to copy transactions spanning a version
+        # commit or abort, or over transactional undos.
+        #
+        # We'll use restore() if it's available, otherwise we'll fall back to
+        # using store().  However, if we use store, then
+        # copyTransactionsFrom() may fail with VersionLockError or
+        # ConflictError.
+        if hasattr(self, 'restore'):
+            restoring = 1
+        else:
+            restoring = 0
         for transaction in other.iterator():
             
             tid=transaction.tid
@@ -252,9 +269,12 @@
             for r in transaction:
                 oid=r.oid
                 if verbose: print `oid`, r.version, len(r.data)
-                pre=preget(oid, None)
-                s=self.store(oid, pre, r.data, r.version, transaction)
-                preindex[oid]=s
+                if restoring:
+                    self.restore(oid, r.serial, r.data, r.version, transaction)
+                else:
+                    pre=preget(oid, None)
+                    s=self.store(oid, pre, r.data, r.version, transaction)
+                    preindex[oid]=s
                 
             self.tpc_vote(transaction)
             self.tpc_finish(transaction)


=== StandaloneZODB/ZODB/FileStorage.py 1.78 => 1.79 ===
 from struct import pack, unpack
 import POSException
-from POSException import UndoError
+from POSException import UndoError, POSKeyError
 from TimeStamp import TimeStamp
 from lock_file import lock_file
 from utils import t32, p64, U64, cp
@@ -574,7 +574,10 @@
             file=self._file
             seek=file.seek
             read=file.read
-            pos=_index[oid]
+            try:
+                pos=_index[oid]
+            except KeyError:
+                raise POSKeyError(oid)
             while 1:
                 seek(pos)
                 h=read(42)
@@ -583,7 +586,8 @@
                 if dserial == serial: break # Yeee ha!
                 # Keep looking for serial
                 pos=U64(prev)
-                if not pos: raise KeyError, serial
+                if not pos:
+                    raise POSKeyError(serial)
                 continue
 
             if vlen:
@@ -682,6 +686,80 @@
         finally:
             self._lock_release()
 
+    def restore(self, oid, serial, data, version, transaction):
+        # A lot like store() but without all the consistency checks.  This
+        # should only be used when we /know/ the data is good, hence the
+        # method name.  While the signature looks like store() there are some
+        # differences:
+        #
+        # - serial is the serial number of /this/ revision, not of the
+        #   previous revision.  It is used instead of self._serial, which is
+        #   ignored.
+        #
+        # - Nothing is returned
+        #
+        # - data can be None, which indicates a George Bailey object (i.e. one
+        #   who's creation has been transactionally undone).
+        if self._is_read_only:
+            raise POSException.ReadOnlyError()
+        if transaction is not self._transaction:
+            raise POSException.StorageTransactionError(self, transaction)
+
+        self._lock_acquire()
+        try:
+            # Position of the non-version data
+            pnv = None
+            # We need to get some information about previous revisions of the
+            # object.  Specifically, we need the position of the non-version
+            # data if this update is in a version.  We also need the position
+            # of the previous record in this version.
+            old = self._index_get(oid, 0)
+            if old:
+                self._file.seek(old)
+                # Read the previous revision record
+                h = self._file.read(42)
+                doid,oserial,sprev,stloc,vlen,splen = unpack(">8s8s8s8sH8s", h)
+                if doid != oid:
+                    raise CorruptedDataError, h
+            # Calculate the file position in the temporary file
+            here = self._pos + self._tfile.tell() + self._thl
+            # And update the temp file index
+            self._tindex[oid] = here
+            # Write the recovery data record
+            if data is None:
+                dlen = 0
+            else:
+                dlen = len(data)
+            self._tfile.write(pack('>8s8s8s8sH8s',
+                                   oid, serial, p64(old), p64(self._pos),
+                                   len(version), p64(dlen)))
+            # We need to write some version information if this revision is
+            # happening in a version.
+            if version:
+                # If there's a previous revision in this version, write the
+                # position, otherwise write the position of the previous
+                # non-version revision.
+                if pnv:
+                    self._tfile.write(pnv)
+                else:
+                    self._tfile.write(p64(old))
+                # Link to the last record for this version
+                pv = self._tvindex.get(version, 0)
+                if not pv:
+                    self._vindex_get(version, 0)
+                self._tfile.write(p64(pv))
+                self._tvindex[version] = here
+                self._tfile.write(version)
+            # And finally, write the data
+            if data is None:
+                # Write a zero backpointer, which is indication used to
+                # represent an un-creation transaction.
+                self._tfile.write(z64)
+            else:
+                self._tfile.write(data)
+        finally:
+            self._lock_release()
+
     def supportsUndo(self):
         return 1
     
@@ -942,7 +1020,7 @@
     def transactionalUndo(self, transaction_id, transaction):
         """Undo a transaction, given by transaction_id.
 
-        Do so by writing new data that reverses tyhe action taken by
+        Do so by writing new data that reverses the action taken by
         the transaction."""        
         # Usually, we can get by with just copying a data pointer, by
         # writing a file position rather than a pickle. Sometimes, we
@@ -1944,7 +2022,8 @@
     
     while 1:
         old=U64(back)
-        if not old: raise KeyError, oid
+        if not old:
+            raise POSKeyError(oid)
         seek(old)
         h=read(42)
         doid,serial,prev,tloc,vlen,plen = unpack(">8s8s8s8sH8s", h)
@@ -1961,7 +2040,8 @@
     
     while 1:
         old=U64(back)
-        if not old: raise KeyError, oid
+        if not old:
+            raise POSKeyError(oid)
         seek(old)
         h=read(42)
         doid,serial,prev,tloc,vlen,plen = unpack(">8s8s8s8sH8s", h)
@@ -2014,6 +2094,8 @@
     """Iterate over the transactions in a FileStorage file.
     """
     _ltid=z64
+
+    _file = None
     
     def __init__(self, file, start=None, stop=None):
         if isinstance(file, StringType):
@@ -2030,6 +2112,12 @@
             self._skip_to_start(start)
         self._stop = stop
 
+    def close(self):
+        file = self._file
+        if file is not None:
+            self._file = None
+            file.close()
+
     def _skip_to_start(self, start):
         # Scan through the transaction records doing almost no sanity
         # checks. 
@@ -2057,6 +2145,10 @@
                           self._file.name, pos, U64(rtl), U64(stl))
 
     def next(self, index=0):
+        if self._file is None:
+            # A closed iterator.  XXX: Is IOError the best we can do?  For
+            # now, mimic a read on a closed file.
+            raise IOError, 'iterator is closed'
         file=self._file
         seek=file.seek
         read=file.read


=== StandaloneZODB/ZODB/POSException.py 1.9 => 1.10 ===
     """
 
+class POSKeyError(KeyError, POSError):
+    """Key not found in database
+    """
+
+    def __str__(self):
+        return "%016x" % utils.U64(self.args[0])
+
 class TransactionError(POSError):
     """An error occured due to normal transaction processing
     """


=== StandaloneZODB/ZODB/fsdump.py 1.1 => 1.2 ===
         j = 0
         for rec in trans:
-            modname, classname = get_pickle_metadata(rec.data)
-            dig = md5.new(rec.data).hexdigest()
-            fullclass = "%s.%s" % (modname, classname)
+            if rec.data is None:
+                fullclass = "undo or abort of object creation"
+            else:
+                modname, classname = get_pickle_metadata(rec.data)
+                dig = md5.new(rec.data).hexdigest()
+                fullclass = "%s.%s" % (modname, classname)
             # special case for testing purposes
             if fullclass == "ZODB.tests.MinPO.MinPO":
                 obj = zodb_unpickle(rec.data)