diff roundup/backends/back_anydbm.py @ 430:350685601f37

Database transactions. . We now have basic transaction support! Information is only written to the database when the commit() method is called. Only the anydbm backend is modified in this way - neither of the bsddb backends have been. The mail, admin and cgi interfaces all use commit (except the admin tool doesn't have a commit command, so interactive users can't commit...) . Fixed login/registration forwarding the user to the right page (or not, on a failure)
author Richard Jones <richard@users.sourceforge.net>
date Sat, 01 Dec 2001 07:17:50 +0000
parents f43af1e97fdd
children a28a80b714f9
line wrap: on
line diff
--- a/roundup/backends/back_anydbm.py	Fri Nov 30 20:47:58 2001 +0000
+++ b/roundup/backends/back_anydbm.py	Sat Dec 01 07:17:50 2001 +0000
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.11 2001-11-21 02:34:18 richard Exp $
+#$Id: back_anydbm.py,v 1.12 2001-12-01 07:17:50 richard Exp $
 
 import anydbm, os, marshal
 from roundup import hyperdb, date, password
@@ -24,7 +24,14 @@
 # Now the database
 #
 class Database(hyperdb.Database):
-    """A database for storing records containing flexible data types."""
+    """A database for storing records containing flexible data types.
+
+    Transaction stuff TODO:
+        . check the timestamp of the class file and nuke the cache if it's
+          modified. Do some sort of conflict checking on the dirty stuff.
+        . perhaps detect write collisions (related to above)?
+
+    """
 
     def __init__(self, storagelocator, journaltag=None):
         """Open a hyperdatabase given a specifier to some storage.
@@ -41,8 +48,10 @@
         """
         self.dir, self.journaltag = storagelocator, journaltag
         self.classes = {}
+        self.cache = {}         # cache of nodes loaded or created
+        self.dirtynodes = {}    # keep track of the dirty nodes by class
+        self.newnodes = {}      # keep track of the new nodes by class
         self.transactions = []
-
     #
     # Classes
     #
@@ -95,39 +104,70 @@
     def addnode(self, classname, nodeid, node):
         ''' add the specified node to its class's db
         '''
-        db = self.getclassdb(classname, 'c')
-        # now save the marshalled data
-        db[nodeid] = marshal.dumps(node)
-        db.close()
-    setnode = addnode
+        self.newnodes.setdefault(classname, {})[nodeid] = 1
+        self.cache.setdefault(classname, {})[nodeid] = node
+        self.savenode(classname, nodeid, node)
+
+    def setnode(self, classname, nodeid, node):
+        ''' change the specified node
+        '''
+        self.dirtynodes.setdefault(classname, {})[nodeid] = 1
+        # can't set without having already loaded the node
+        self.cache[classname][nodeid] = node
+        self.savenode(classname, nodeid, node)
+
+    def savenode(self, classname, nodeid, node):
+        ''' perform the saving of data specified by the set/addnode
+        '''
+        self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
 
     def getnode(self, classname, nodeid, cldb=None):
         ''' add the specified node to its class's db
         '''
+        # try the cache
+        cache = self.cache.setdefault(classname, {})
+        if cache.has_key(nodeid):
+            return cache[nodeid]
+
+        # get from the database and save in the cache
         db = cldb or self.getclassdb(classname)
         if not db.has_key(nodeid):
             raise IndexError, nodeid
         res = marshal.loads(db[nodeid])
         if not cldb: db.close()
+        cache[nodeid] = res
         return res
 
     def hasnode(self, classname, nodeid, cldb=None):
         ''' add the specified node to its class's db
         '''
+        # try the cache
+        cache = self.cache.setdefault(classname, {})
+        if cache.has_key(nodeid):
+            return 1
+
+        # not in the cache - check the database
         db = cldb or self.getclassdb(classname)
         res = db.has_key(nodeid)
         if not cldb: db.close()
         return res
 
     def countnodes(self, classname, cldb=None):
+        # include the new nodes not saved to the DB yet
+        count = len(self.newnodes.get(classname, {}))
+
+        # and count those in the DB
         db = cldb or self.getclassdb(classname)
-        return len(db.keys())
+        count = count + len(db.keys())
         if not cldb: db.close()
-        return res
+        return count
 
     def getnodeids(self, classname, cldb=None):
+        # start off with the new nodes
+        res = self.newnodes.get(classname, {}).keys()
+
         db = cldb or self.getclassdb(classname)
-        res = db.keys()
+        res = res + db.keys()
         if not cldb: db.close()
         return res
 
@@ -142,17 +182,8 @@
             'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
             'retire' -- 'params' is None
         '''
-        entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
-            params)
-        db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname), 'c')
-        if db.has_key(nodeid):
-            s = db[nodeid]
-            l = marshal.loads(db[nodeid])
-            l.append(entry)
-        else:
-            l = [entry]
-        db[nodeid] = marshal.dumps(l)
-        db.close()
+        self.transactions.append((self._doSaveJournal, (classname, nodeid,
+            action, params)))
 
     def getjournal(self, classname, nodeid):
         ''' get the journal for id
@@ -175,9 +206,10 @@
         return res
 
     def close(self):
-        ''' Close the Database - we must release the circular refs so that
-            we can be del'ed and the underlying anydbm connections closed
-            cleanly.
+        ''' Close the Database.
+        
+            Commit all data to the database and release circular refs so
+            the database is closed cleanly.
         '''
         self.classes = {}
 
@@ -189,18 +221,50 @@
         ''' Commit the current transactions.
         '''
         # lock the DB
-        for action, classname, entry in self.transactions:
-            # write the node, figure what's changed for the journal.
-            pass
+        for method, args in self.transactions:
+            print method.__name__, args
+            # TODO: optimise this, duh!
+            method(*args)
         # unlock the DB
 
+        # all transactions committed, back to normal
+        self.cache = {}
+        self.dirtynodes = {}
+        self.newnodes = {}
+        self.transactions = []
+
+    def _doSaveNode(self, classname, nodeid, node):
+        db = self.getclassdb(classname, 'c')
+        # now save the marshalled data
+        db[nodeid] = marshal.dumps(node)
+        db.close()
+
+    def _doSaveJournal(self, classname, nodeid, action, params):
+        entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
+            params)
+        db = anydbm.open(os.path.join(self.dir, 'journals.%s'%classname), 'c')
+        if db.has_key(nodeid):
+            s = db[nodeid]
+            l = marshal.loads(db[nodeid])
+            l.append(entry)
+        else:
+            l = [entry]
+        db[nodeid] = marshal.dumps(l)
+        db.close()
+
     def rollback(self):
         ''' Reverse all actions from the current transaction.
         '''
+        self.cache = {}
+        self.dirtynodes = {}
+        self.newnodes = {}
         self.transactions = []
 
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.11  2001/11/21 02:34:18  richard
+#Added a target version field to the extended issue schema
+#
 #Revision 1.10  2001/10/09 23:58:10  richard
 #Moved the data stringification up into the hyperdb.Class class' get, set
 #and create methods. This means that the data is also stringified for the

Roundup Issue Tracker: http://roundup-tracker.org/