[Zope-Checkins] CVS: ZODB3/ZODB - fsrecover.py:1.5.8.2

Jeremy Hylton jeremy@zope.com
Fri, 23 May 2003 12:33:09 -0400


Update of /cvs-repository/ZODB3/ZODB
In directory cvs.zope.org:/tmp/cvs-serv3450/ZODB

Modified Files:
      Tag: ZODB3-3_1-branch
	fsrecover.py 
Log Message:
Fix some serious bugs in fsrecover.

When a corrupted record is found, it guesses a location for the next
good transaction header.  If the guessed location happened to have a
'c' character at just the right spot, it would think it was at the end
of the file.

Fix in off-by-one bug in scan() that lead to failure.

Do more sanity checks for invalid transaction headers following a
scan.

Use restore() instead of store() to copy data.  This change should
maximize the chance that the recovered storage is identical except for
the damaged records.

Add tests from the trunk and remove test in testFileStorage.


=== ZODB3/ZODB/fsrecover.py 1.5.8.1 => 1.5.8.2 ===
--- ZODB3/ZODB/fsrecover.py:1.5.8.1	Mon Mar 17 14:39:14 2003
+++ ZODB3/ZODB/fsrecover.py	Fri May 23 12:32:38 2003
@@ -11,8 +11,6 @@
 # FOR A PARTICULAR PURPOSE
 #
 ##############################################################################
-
-
 """Simple script for repairing damaged FileStorage files.
 
 Usage: %s [-f] input output
@@ -84,220 +82,272 @@
 
 import getopt, ZODB.FileStorage, struct, time
 from struct import unpack
-from ZODB.utils import t32, p64, U64
+from ZODB.utils import t32, p64, u64
 from ZODB.TimeStamp import TimeStamp
 from cPickle import loads
 from ZODB.FileStorage import RecordIterator
 
-class EOF(Exception): pass
 class ErrorFound(Exception): pass
 
 def error(mess, *args):
     raise ErrorFound(mess % args)
 
-def read_transaction_header(file, pos, file_size):
+def read_txn_header(f, pos, file_size, outp, ltid):
     # Read the transaction record
-    seek=file.seek
-    read=file.read
-
-    seek(pos)
-    h=read(23)
-    if len(h) < 23: raise EOF
+    f.seek(pos)
+    h = f.read(23)
+    if len(h) < 23:
+        raise EOFError
 
     tid, stl, status, ul, dl, el = unpack(">8s8scHHH",h)
     if el < 0: el=t32-el
 
-    tl=U64(stl)
-
-    if status=='c': raise EOF
+    tl = u64(stl)
 
-    if pos+(tl+8) > file_size:
+    if pos + (tl + 8) > file_size:
         error("bad transaction length at %s", pos)
 
-    if status not in ' up':
-        error('invalid status, %s, at %s', status, pos)
+    if tl < (23 + ul + dl + el):
+        error("invalid transaction length, %s, at %s", tl, pos)
 
-    if tl < (23+ul+dl+el):
-        error('invalid transaction length, %s, at %s', tl, pos)
+    if ltid and tid < ltid:
+        error("time-stamp reducation %s < %s, at %s", u64(tid), u64(ltid), pos)
 
-    tpos=pos
-    tend=tpos+tl
+    if status == "c":
+        truncate(f, pos, file_size, output)
+        raise EOFError
 
-    if status=='u':
+    if status not in " up":
+        error("invalid status, %r, at %s", status, pos)
+
+    tpos = pos
+    tend = tpos + tl
+
+    if status == "u":
         # Undone transaction, skip it
-        seek(tend)
-        h=read(8)
-        if h != stl: error('inconsistent transaction length at %s', pos)
-        pos=tend+8
-        return pos, None
-
-    pos=tpos+(23+ul+dl+el)
-    user=read(ul)
-    description=read(dl)
+        f.seek(tend)
+        h = f.read(8)
+        if h != stl:
+            error("inconsistent transaction length at %s", pos)
+        pos = tend + 8
+        return pos, None, tid
+
+    pos = tpos+(23+ul+dl+el)
+    user = f.read(ul)
+    description = f.read(dl)
     if el:
-        try: e=loads(read(el))
+        try: e=loads(f.read(el))
         except: e={}
     else: e={}
 
     result = RecordIterator(tid, status, user, description, e, pos, tend,
-                            file, tpos)
-    pos=tend
+                            f, tpos)
+    pos = tend
 
     # Read the (intentionally redundant) transaction length
-    seek(pos)
-    h=read(8)
+    f.seek(pos)
+    h = f.read(8)
     if h != stl:
         error("redundant transaction length check failed at %s", pos)
-    pos=pos+8
+    pos += 8
 
-    return pos, result
+    return pos, result, tid
 
-def scan(file, pos, file_size):
-    seek=file.seek
-    read=file.read
+def truncate(f, pos, file_size, outp):
+    """Copy data from pos to end of f to a .trNNN file."""
+
+    i = 0
     while 1:
-        seek(pos)
-        data=read(8096)
-        if not data: return 0
+        trname = outp + ".tr%d" % i
+        if os.path.exists(trname):
+            i += 1
+    tr = open(trname, "wb")
+    copy(f, tr, file_size - pos)
+    f.seek(pos)
+    tr.close()
+
+def copy(src, dst, n):
+    while n:
+        buf = src.read(8096)
+        if not buf:
+            break
+        if len(buf) > n:
+            buf = buf[:n]
+        dst.write(buf)
+        n -= len(buf)
+
+def scan(f, pos):
+    """Return a potential transaction location following pos in f.
+
+    This routine scans forward from pos looking for the last data
+    record in a transaction.  A period '.' always occurs at the end of
+    a pickle, and an 8-byte transaction length follows the last
+    pickle.  If a period is followed by a plausible 8-byte transaction
+    length, assume that we have found the end of a transaction.
+
+    The caller should try to verify that the returned location is
+    actually a transaction header.
+    """
+    while 1:
+        f.seek(pos)
+        data = f.read(8096)
+        if not data:
+            return 0
 
-        s=0
+        s = 0
         while 1:
-            l=data.find('.', s)
+            l = data.find(".", s)
             if l < 0:
-                pos=pos+8096
+                pos += len(data)
                 break
-            if l > 8080:
-                pos = pos + l
+            # If we are less than 8 bytes from the end of the
+            # string, we need to read more data.
+            s = l + 1
+            if s > len(data) - 8:
+                pos += l
                 break
-            s=l+1
-            tl=U64(data[s:s+8])
+            tl = u64(data[s:s+8])
             if tl < pos:
                 return pos + s + 8
 
 def iprogress(i):
-    if i%2: print '.',
-    else: print (i/2)%10,
+    if i % 2:
+        print ".",
+    else:
+        print (i/2) % 10,
     sys.stdout.flush()
 
 def progress(p):
-    for i in range(p): iprogress(i)
-
-def recover(argv=sys.argv):
+    for i in range(p):
+        iprogress(i)
 
+def main():
     try:
-        opts, (inp, outp) = getopt.getopt(argv[1:], 'fv:pP:')
-        force = partial = verbose = 0
-        pack = None
-        for opt, v in opts:
-            if opt == '-v': verbose = int(v)
-            elif opt == '-p': partial=1
-            elif opt == '-f': force=1
-            elif opt == '-P': pack=time.time()-float(v)
-
-
-        force = filter(lambda opt: opt[0]=='-f', opts)
-        partial = filter(lambda opt: opt[0]=='-p', opts)
-        verbose = filter(lambda opt: opt[0]=='-v', opts)
-        verbose = verbose and int(verbose[0][1]) or 0
-        print 'Recovering', inp, 'into', outp
-    except:
+        opts, (inp, outp) = getopt.getopt(sys.argv[1:], "fv:pP:")
+    except getopt.error:
         die()
         print __doc__ % argv[0]
+        
+    force = partial = verbose = 0
+    pack = None
+    for opt, v in opts:
+        if opt == "-v":
+            verbose = int(v)
+        elif opt == "-p":
+            partial = 1
+        elif opt == "-f":
+            force = 1
+        elif opt == "-P":
+            pack = time.time() - float(v)
 
+    recover(inp, outp, verbose, partial, force, pack)
+
+def recover(inp, outp, verbose=0, partial=0, force=0, pack=0):
+    print "Recovering", inp, "into", outp
 
     if os.path.exists(outp) and not force:
         die("%s exists" % outp)
 
-    file=open(inp, "rb")
-    seek=file.seek
-    read=file.read
-    if read(4) != ZODB.FileStorage.packed_version:
+    f = open(inp, "rb")
+    if f.read(4) != ZODB.FileStorage.packed_version:
         die("input is not a file storage")
 
-    seek(0,2)
-    file_size=file.tell()
+    f.seek(0,2)
+    file_size = f.tell()
 
-    ofs=ZODB.FileStorage.FileStorage(outp, create=1)
-    _ts=None
-    ok=1
-    prog1=0
-    preindex={}; preget=preindex.get   # waaaa
-    undone=0
+    ofs = ZODB.FileStorage.FileStorage(outp, create=1)
+    _ts = None
+    ok = 1
+    prog1 = 0
+    undone = 0
 
-    pos=4
+    pos = 4
+    ltid = None
     while pos:
-
         try:
-            npos, transaction = read_transaction_header(file, pos, file_size)
-        except EOF:
+            npos, txn, tid = read_txn_header(f, pos, file_size, outp, ltid)
+        except EOFError:
             break
-        except:
-            print "\n%s: %s\n" % sys.exc_info()[:2]
-            if not verbose: progress(prog1)
-            pos = scan(file, pos, file_size)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception, err:
+            print "error reading txn header:", err
+            if not verbose:
+                progress(prog1)
+            pos = scan(f, pos)
+            if verbose > 1:
+                print "looking for valid txn header at", pos
             continue
+        ltid = tid
 
-        if transaction is None:
+        if txn is None:
             undone = undone + npos - pos
-            pos=npos
+            pos = npos
             continue
         else:
-            pos=npos
+            pos = npos
 
-        tid=transaction.tid
+        tid = txn.tid
 
         if _ts is None:
-            _ts=TimeStamp(tid)
+            _ts = TimeStamp(tid)
         else:
-            t=TimeStamp(tid)
+            t = TimeStamp(tid)
             if t <= _ts:
-                if ok: print ('Time stamps out of order %s, %s' % (_ts, t))
-                ok=0
-                _ts=t.laterThan(_ts)
-                tid=`_ts`
+                if ok:
+                    print ("Time stamps out of order %s, %s" % (_ts, t))
+                ok = 0
+                _ts = t.laterThan(_ts)
+                tid = `_ts`
             else:
                 _ts = t
                 if not ok:
-                    print ('Time stamps back in order %s' % (t))
-                    ok=1
-
-        if verbose:
-            print 'begin',
-            if verbose > 1: print
-            sys.stdout.flush()
+                    print ("Time stamps back in order %s" % (t))
+                    ok = 1
 
-        ofs.tpc_begin(transaction, tid, transaction.status)
+        ofs.tpc_begin(txn, tid, txn.status)
 
         if verbose:
-            print 'begin', pos, _ts,
-            if verbose > 1: print
+            print "begin", pos, _ts,
+            if verbose > 1:
+                print
             sys.stdout.flush()
 
-        nrec=0
+        nrec = 0
         try:
-            for r in transaction:
-                oid=r.oid
-                if verbose > 1: print U64(oid), r.version, len(r.data)
-                pre=preget(oid, None)
-                s=ofs.store(oid, pre, r.data, r.version, transaction)
-                preindex[oid]=s
-                nrec=nrec+1
-        except:
+            for r in txn:
+                if verbose > 1:
+                    if r.data is None:
+                        l = "bp"
+                    else:
+                        l = len(r.data)
+                        
+                    print "%7d %s %s" % (u64(r.oid), l, r.version)
+                s = ofs.restore(r.oid, r.serial, r.data, r.version,
+                                r.data_txn, txn)
+                nrec += 1
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception, err:
             if partial and nrec:
-                ofs._status='p'
-                ofs.tpc_vote(transaction)
-                ofs.tpc_finish(transaction)
-                if verbose: print 'partial'
+                ofs._status = "p"
+                ofs.tpc_vote(txn)
+                ofs.tpc_finish(txn)
+                if verbose:
+                    print "partial"
             else:
-                ofs.tpc_abort(transaction)
-            print "\n%s: %s\n" % sys.exc_info()[:2]
-            if not verbose: progress(prog1)
-            pos = scan(file, pos, file_size)
+                ofs.tpc_abort(txn)
+            print "error copying transaction:", err
+            if not verbose:
+                progress(prog1)
+            pos = scan(f, pos)
+            if verbose > 1:
+                print "looking for valid txn header at", pos
         else:
-            ofs.tpc_vote(transaction)
-            ofs.tpc_finish(transaction)
+            ofs.tpc_vote(txn)
+            ofs.tpc_finish(txn)
             if verbose:
-                print 'finish'
+                print "finish"
                 sys.stdout.flush()
 
         if not verbose:
@@ -320,5 +370,6 @@
 
     ofs.close()
 
+if __name__ == "__main__":
+    main()
 
-if __name__=='__main__': recover()