Code

Enabled transaction support in the bsddb backend. It uses the anydbm code
[roundup.git] / roundup / backends / back_anydbm.py
index 99febb29bdcefe877885d2edff7d082ab5ee4e05..6a90a1d9404b5c0a24c8d1cc240c04c1f6251342 100644 (file)
@@ -1,14 +1,43 @@
-#$Id: back_anydbm.py,v 1.3 2001-07-25 01:23:07 richard Exp $
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+# 
+#$Id: back_anydbm.py,v 1.14 2001-12-10 22:20:01 richard Exp $
+'''
+This module defines a backend that saves the hyperdatabase in a database
+chosen by anydbm. It is guaranteed to always be available in python
+versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
+serious bugs, and is not available)
+'''
 
 import anydbm, os, marshal
-from roundup import hyperdb, date
+from roundup import hyperdb, date, password
 
 #
 # 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.
 
@@ -24,6 +53,10 @@ class Database(hyperdb.Database):
         """
         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
@@ -77,59 +110,67 @@ class Database(hyperdb.Database):
     def addnode(self, classname, nodeid, node):
         ''' add the specified node to its class's db
         '''
-        db = self.getclassdb(classname, 'c')
+        self.newnodes.setdefault(classname, {})[nodeid] = 1
+        self.cache.setdefault(classname, {})[nodeid] = node
+        self.savenode(classname, nodeid, node)
 
-        # convert the instance data to builtin types
-        properties = self.classes[classname].properties
-        for key in properties.keys():
-            if properties[key].isDateType:
-                node[key] = node[key].get_tuple()
-            elif properties[key].isIntervalType:
-                node[key] = node[key].get_tuple()
+    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)
 
-        # now save the marshalled data
-        db[nodeid] = marshal.dumps(node)
-        db.close()
-    setnode = addnode
+    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])
-
-        # convert the marshalled data to instances
-        properties = self.classes[classname].properties
-        for key in res.keys():
-            if key == self.RETIRED_FLAG: continue
-            if properties[key].isDateType:
-                res[key] = date.Date(res[key])
-            elif properties[key].isIntervalType:
-                res[key] = date.Interval(res[key])
-
-        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())
-        if not cldb: db.close()
-        return res
+        count = count + len(db.keys())
+        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()
-        if not cldb: db.close()
+        res = res + db.keys()
         return res
 
     #
@@ -143,17 +184,8 @@ class Database(hyperdb.Database):
             '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
@@ -172,35 +204,112 @@ class Database(hyperdb.Database):
             (nodeid, date_stamp, self.journaltag, action, params) = entry
             date_obj = date.Date(date_stamp)
             res.append((nodeid, date_obj, self.journaltag, action, params))
-        db.close()
         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.
-        '''
-        self.classes = None
-
 
     #
     # Basic transaction support
     #
-    # TODO: well, write these methods (and then use them in other code)
-    def register_action(self):
-        ''' Register an action to the transaction undo log
-        '''
-
     def commit(self):
-        ''' Commit the current transaction, start a new one
+        ''' Commit the current transactions.
         '''
+        # lock the DB
+        for method, args in self.transactions:
+            # 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
+        ''' Reverse all actions from the current transaction.
         '''
+        self.cache = {}
+        self.dirtynodes = {}
+        self.newnodes = {}
+        self.transactions = []
 
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.13  2001/12/02 05:06:16  richard
+#. We now use weakrefs in the Classes to keep the database reference, so
+#  the close() method on the database is no longer needed.
+#  I bumped the minimum python requirement up to 2.1 accordingly.
+#. #487480 ] roundup-server
+#. #487476 ] INSTALL.txt
+#
+#I also cleaned up the change message / post-edit stuff in the cgi client.
+#There's now a clearly marked "TODO: append the change note" where I believe
+#the change note should be added there. The "changes" list will obviously
+#have to be modified to be a dict of the changes, or somesuch.
+#
+#More testing needed.
+#
+#Revision 1.12  2001/12/01 07:17:50  richard
+#. 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)
+#
+#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
+#journal call, and removes duplication of code from the backends. The
+#backend code now only sees strings.
+#
+#Revision 1.9  2001/10/09 07:25:59  richard
+#Added the Password property type. See "pydoc roundup.password" for
+#implementation details. Have updated some of the documentation too.
+#
+#Revision 1.8  2001/09/29 13:27:00  richard
+#CGI interfaces now spit up a top-level index of all the instances they can
+#serve.
+#
+#Revision 1.7  2001/08/12 06:32:36  richard
+#using isinstance(blah, Foo) now instead of isFooType
+#
+#Revision 1.6  2001/08/07 00:24:42  richard
+#stupid typo
+#
+#Revision 1.5  2001/08/07 00:15:51  richard
+#Added the copyright/license notice to (nearly) all files at request of
+#Bizar Software.
+#
+#Revision 1.4  2001/07/30 01:41:36  richard
+#Makes schema changes mucho easier.
+#
+#Revision 1.3  2001/07/25 01:23:07  richard
+#Added the Roundup spec to the new documentation directory.
+#
 #Revision 1.2  2001/07/23 08:20:44  richard
 #Moved over to using marshal in the bsddb and anydbm backends.
 #roundup-admin now has a "freshen" command that'll load/save all nodes (not