commit-gnue
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[gnue] r6876 - trunk/gnue-appserver/src


From: johannes
Subject: [gnue] r6876 - trunk/gnue-appserver/src
Date: Sun, 9 Jan 2005 13:10:18 -0600 (CST)

Author: johannes
Date: 2005-01-09 13:10:16 -0600 (Sun, 09 Jan 2005)
New Revision: 6876

Modified:
   trunk/gnue-appserver/src/data.py
   trunk/gnue-appserver/src/geasInstance.py
   trunk/gnue-appserver/src/geasList.py
   trunk/gnue-appserver/src/geasSession.py
Log:
Implemented dirty reads; OnValidate also validates instances changed by 
validation; better handling of cached instances from 'unrelated blocks'


Modified: trunk/gnue-appserver/src/data.py
===================================================================
--- trunk/gnue-appserver/src/data.py    2005-01-07 08:05:02 UTC (rev 6875)
+++ trunk/gnue-appserver/src/data.py    2005-01-09 19:10:16 UTC (rev 6876)
@@ -37,6 +37,12 @@
           % {'table': table, 'row': row}
     errors.SystemError.__init__ (self, msg)
 
+class InvalidCacheError (errors.SystemError):
+  def __init__ (self, table, row, state):
+    msg = u_("Row '%(row)s' of table '%(table)s' has an invalid state "
+             "%(state)s'") \
+          % {'table': table, 'row': row, 'state': state}
+    errors.SystemError.__init__ (self, msg)
 
 # =============================================================================
 # Cache class
@@ -237,16 +243,31 @@
   # Clear the whole cache
   # ---------------------------------------------------------------------------
 
-  def clear (self):
+  def clear (self, oldOnly = False):
     """
     Forget all data in the cache, original values as well as dirty values.
     """
 
-    self.__old   = {}
-    self.__new   = {}
-    self.__state = {}
+    if oldOnly:
+      # on a commit we need to remove all 'commited' stuff from cache, in order
+      # to get new data from other transactions in.
+      for table in self.__old.keys ():
+        for row in self.__old [table].keys ():
+          # state of an 'old' row is empty if it has been deleted or if it's
+          # really clean.
+          if self.status (table, row) == '':
+            del self.__old [table][row]
 
+        # if a table has no more rows, remove it too
+        if not self.__old [table].keys ():
+          del self.__old [table]
 
+    else:
+      self.__old   = {}
+      self.__new   = {}
+      self.__state = {}
+
+
   # ---------------------------------------------------------------------------
   # Update the state information of a given row
   # ---------------------------------------------------------------------------
@@ -331,7 +352,87 @@
       self.__setState (table, row, 'commitable')
 
 
+  # ---------------------------------------------------------------------------
+  # Make the given row in a table to be treated as 'clean'
+  # ---------------------------------------------------------------------------
 
+  def makeClean (self, table, row):
+    """
+    This function makes a row of a table 'clean'. It will be moved from the
+    dirty into the clean cache
+
+    @param table: name of the table
+    @param row: gnue_id of the row to be moved
+    """
+
+    if self.__new.has_key (table) and self.__new [table].has_key (row):
+      if not self.__old.has_key (table):
+        self.__old [table] = {}
+
+      self.__old [table] [row] = self.__new [table] [row]
+
+    self.__removeFromDict (self.__new, table, row)
+    self.__removeFromDict (self.__state, table, row)
+
+
+  # ---------------------------------------------------------------------------
+  # Remove a row of a table completely from the cache
+  # ---------------------------------------------------------------------------
+
+  def remove (self, table, row):
+    """
+    This function removes the given row of the table completely from the cache,
+    no matter wether it's dirty or not.
+
+    @param table: name of the table
+    @param row: gnue_id of the row to be removed from the cache
+    """
+
+    self.__removeFromDict (self.__new, table, row)
+    self.__removeFromDict (self.__old, table, row)
+    self.__removeFromDict (self.__state, table, row)
+
+
+  # ---------------------------------------------------------------------------
+  # Remove a row of a table from a given cache-dictionary
+  # ---------------------------------------------------------------------------
+
+  def __removeFromDict (self, dictionary, table, row):
+    """
+    This function removes a row of a table from the given cache-dictionary. If
+    the specified row was the last row of the table cache, the table-dictionary
+    will be removed too.
+
+    @param dictionary: cache-dictionary: dict [table][row][field]
+    @param table: name of the table to remove a row from
+    @param row: gnue_id of the row to be removed
+    """
+
+    if dictionary.has_key (table) and dictionary [table].has_key (row):
+      del dictionary [table] [row]
+
+    if dictionary.has_key (table) and not len (dictionary [table].keys ()):
+      del dictionary [table]
+
+
+  # ---------------------------------------------------------------------------
+  # Have a look if the cache is really in a clean state
+  # ---------------------------------------------------------------------------
+
+  def _assertClean (self):
+    """
+    This function iterates over all 'new' records in the cache and verifies if
+    they're in a clean state.
+    """
+
+    for table in self.__new.keys ():
+      for row in self.__new [table].keys ():
+        if not self.status (table, row) in ["initialized"]:
+          raise InvalidCacheError, (table, row, self.status (table, row))
+
+    
+
+
 # =============================================================================
 # Helper methods
 # =============================================================================
@@ -485,7 +586,7 @@
         of the foreign key (i.e. of the referencing field).  The primary key is
         of course always 'gnue_id'. For a single element in the dictionary
         (namely the main table), 'fk_alias' and 'fk_field' both are None.
-        Note that an inner join will be executet, that means ony results where
+        Note that an inner join will be executet, that means only results where
         all references can be resolved (and are non-NULL) will be returned.
 
         Finally, 'fields' is a list of field names to be included in the query.
@@ -516,6 +617,10 @@
       checktype (fields, ListType)
       for fields_element in fields: checktype (fields_element, UnicodeType)
 
+    # Make sure the following query have all pending changes available at the
+    # backend
+    self.postChanges ()
+
     return recordset (self.__cache, self.__connections, self.__database,
                       content, conditions, order)
 
@@ -620,20 +725,22 @@
 
     return r
 
+
   # ---------------------------------------------------------------------------
-  # Write all changes back to the database
+  # Post all pending changes to the backend
   # ---------------------------------------------------------------------------
 
-  def commit (self):
+  def postChanges (self):
     """
-    Write all dirty data to the database backend by a single transaction that
-    is committed immediately. This operation invalidates the cache.
+    Write all dirty data to the database backend without committing the current
+    running transactions. All changes are treated to be 'clean' afterwards.
     """
 
     tables = self.__cache.dirtyTables ()
 
     # first perform all inserts
-    for (table, row) in self.__inserted:
+    for (table, row) in self.__inserted [:]:
+      x = self.__cache.status (table, row)
       if self.__cache.status (table, row) == 'inserted':
         fields = tables [table] [row]
         resultSet = _createEmptyResultSet (self.__connections,
@@ -650,9 +757,13 @@
         finally:
           resultSet.close ()
 
-    self.__inserted         = []
-    self.__confirmedInserts = []
+        self.__inserted.remove ((table, row))
 
+        if (table, row) in self.__confirmedInserts:
+          self.__confirmedInserts.remove ((table, row))
+
+        self.__cache.makeClean (table, row)
+
     # second perform all updates
     for (table, rows) in tables.items ():
       for (row, fields) in rows.items ():
@@ -674,6 +785,8 @@
           finally:
             resultSet.close ()
 
+          self.__cache.makeClean (table, row)
+
     # perform all deletes
     for (table, row) in self.__deleted:
       # TODO: gnue-common should provide a method for deleting a record
@@ -688,18 +801,35 @@
       finally:
         resultSet.close ()
 
+      self.__cache.remove (table, row)
+
     self.__deleted = []
     self.__confirmedDeletes = []
 
 
+
+  # ---------------------------------------------------------------------------
+  # Write all changes back to the database
+  # ---------------------------------------------------------------------------
+
+  def commit (self):
+    """
+    Write all dirty data to the database backend by a single transaction that
+    is committed immediately. This operation invalidates the cache.
+    """
+    self.postChanges ()
+
+    # Assert
+    self.__cache._assertClean ()
+
     # Commit the whole transaction
     self.__connections.commitAll ()
 
     # The transaction has ended. Changes from other transactions could become
-    # valid in this moment, so we have to clear the whole cache.
-    self.__cache.clear ()
-    self.__confirmedCache = None
+    # valid in this moment, so we have to clear the cache.
+    self.__cache.clear (True)
 
+
   # ---------------------------------------------------------------------------
   # Undo all changes
   # ---------------------------------------------------------------------------
@@ -785,10 +915,10 @@
 
   def __init__ (self, cache, connections, database, content, conditions,
                 order):
-    self.__cache = cache
+    self.__cache       = cache
     self.__connections = connections
-    self.__database = database
-    self.__content = content
+    self.__database    = database
+    self.__content     = content
 
     # make sure gnue_id is selected for all tables
     for (alias, (table, fk_alias, fk_field, fields)) in self.__content.items():
@@ -814,7 +944,7 @@
     result = None
     records = {}                        # the record for each alias
 
-    # iterate through all tables invoved in the query
+    # iterate through all tables involved in the query
     for (alias, (table, fk_alias, fk_field, fields)) in self.__content.items():
 
       # find id of the record
@@ -880,13 +1010,13 @@
   def close (self):
     """
     This function closes a record set which is no longer needed. It closes the
-    underlying result set and clears it's cache.
+    underlying result set.
     """
 
     if self.__resultSet is not None:
       self.__resultSet.close ()
+
     self.__resultSet = None
-    # self.__cache.clear ()
 
 
 # =============================================================================

Modified: trunk/gnue-appserver/src/geasInstance.py
===================================================================
--- trunk/gnue-appserver/src/geasInstance.py    2005-01-07 08:05:02 UTC (rev 
6875)
+++ trunk/gnue-appserver/src/geasInstance.py    2005-01-09 19:10:16 UTC (rev 
6876)
@@ -25,6 +25,7 @@
 import string
 import mx.DateTime
 import mx.DateTime.ISO
+
 from gnue import appserver
 from gnue.appserver.language import Object, Session
 from gnue.common.logic.language import getLanguageAdapter
@@ -80,6 +81,9 @@
              'part': part}
     errors.ApplicationError.__init__ (self, msg)
 
+class OrderBySequenceError (errors.ApplicationError):
+  pass
+
 # =============================================================================
 # Instance class
 # =============================================================================
@@ -433,3 +437,70 @@
     """
 
     self.__record.touch (dirty)
+
+
+  # ---------------------------------------------------------------------------
+  # Get the fully qualified classname of an instance
+  # ---------------------------------------------------------------------------
+
+  def getClassname (self):
+    """
+    This function returns the fully qualified classname of the instance
+    @return: fully qualified classname of the instance
+    """
+
+    return self.__classdef.fullName
+
+
+  # ---------------------------------------------------------------------------
+  # Set a sequence of fields to order instances
+  # ---------------------------------------------------------------------------
+
+  def setSort (self, order):
+    """
+    Set a sequence of tuples used for comparing this instance with another one.
+
+    @param order: sequence of tuples (value, descending), where data is
+        the value to be compared and 'descending' specifies the sort-direction.
+    """
+
+    checktype (order, [types.NoneType, types.ListType])
+    self.__order = order
+
+
+  # ---------------------------------------------------------------------------
+  # Compare two instances
+  # ---------------------------------------------------------------------------
+
+  def __cmp__ (self, other):
+
+    # If this or the other instance has no order-by rule, just do the
+    # default-compare for instances
+    if self.__order is None or other.__order is None:
+      return cmp (self, other)
+
+    # If both instance have an order-by rule, they must match in length
+    if len (self.__order) != len (other.__order):
+      raise OrderBySequenceError, \
+          u_("Order-by sequence mismatch: '%(self)s' and '%(other)s'") \
+          % {'self': self.__order, 'other': other.__order}
+
+    for ix in xrange (len (self.__order)):
+      (left, descending)  = self.__order [ix]
+      (right, rightOrder) = other.__order [ix]
+
+      if descending != rightOrder:
+        raise OrderBySequenceError, \
+            u_("Order-by sequence element has different directions: "
+               "'%(self)s', '%(other)s'") \
+            % {'self': self.__order [ix], 'other': other.__order [ix]}
+
+      if descending:
+        (left, right) = (right, left)
+
+      result = cmp (left, right)
+      if result != 0:
+        return result
+
+    # If no field gave a result, the two instances are treated to be equal
+    return 0

Modified: trunk/gnue-appserver/src/geasList.py
===================================================================
--- trunk/gnue-appserver/src/geasList.py        2005-01-07 08:05:02 UTC (rev 
6875)
+++ trunk/gnue-appserver/src/geasList.py        2005-01-09 19:10:16 UTC (rev 
6876)
@@ -23,6 +23,7 @@
 
 import geasInstance
 import string
+import time
 
 # =============================================================================
 # List class
@@ -57,6 +58,8 @@
 
   def __fillupFunc (self, count):
     """
+    This function calls an apropriate method to fill up the internal sequence
+    of instances, depending wether appserver has to do sorting or not.
     """
 
     if len (self.__asSorting):
@@ -195,12 +198,23 @@
     """
     This function sorts the unsorted batch according to asSorting.
     """
-    if len (self.__asSorting):
-      slist = [i.get (self.__asSorting) + [i] for i in self.__unsorted]
-      slist.sort ()
-      self.__unsorted = [item [-1] for item in slist]
 
+    if len (self.__asSorting) and len (self.__unsorted) > 1:
 
+      # We do support only 'ascending' order by now, but as soon the order-by
+      # statement in common changes, we can add this here
+      for i in self.__unsorted:
+        # NOTE: building a sort-sequence this way is not optimal. If there are
+        # multiple calculated fields in an order-by all of them will be
+        # executed. Even if a comparison would be able without them. I'll fix
+        # this with adding support for descending order.
+        i.setSort ([(v, False) for v in i.get (self.__asSorting)])
+
+
+      self.__unsorted.sort ()
+
+
+
   # ---------------------------------------------------------------------------
   # Get the length of the list (the number of entries)
   # ---------------------------------------------------------------------------

Modified: trunk/gnue-appserver/src/geasSession.py
===================================================================
--- trunk/gnue-appserver/src/geasSession.py     2005-01-07 08:05:02 UTC (rev 
6875)
+++ trunk/gnue-appserver/src/geasSession.py     2005-01-09 19:10:16 UTC (rev 
6876)
@@ -80,6 +80,11 @@
              'value': filterValue}
     errors.AdminError.__init__ (self, msg)
 
+class ValidationCyclesError (errors.ApplicationError):
+  def __init__ (self, classlist):
+    msg = u_("Maximum validation cycle reached. Classes in current cycle: %s")\
+          % string.join (classlist, ", ")
+    errors.ApplicationError.__init__ (self, msg)
 
 # =============================================================================
 # Session class
@@ -87,6 +92,8 @@
 
 class geasSession:
 
+  _MAX_CYCLES = 50      # Number of OnValidate cycles
+
   # ---------------------------------------------------------------------------
   # Initalize
   # ---------------------------------------------------------------------------
@@ -130,11 +137,15 @@
     return self.sm.classes [classname]
 
 
+  # ---------------------------------------------------------------------------
+  # Get a procedure definition from a classdefinition
+  # ---------------------------------------------------------------------------
 
   def __getProcdef (self, classdef, procedurename):
     # add access control to procedures here
     return classdef.procedures [procedurename]
 
+
   # ---------------------------------------------------------------------------
   # Get a field name from a property name, resolving references
   # ---------------------------------------------------------------------------
@@ -278,15 +289,40 @@
   # ---------------------------------------------------------------------------
 
   def commit (self):
+    """
+    This function commit the currently running transaction. But before the
+    backend will be requested to do so, all dirty instances are validated.
+    """
 
     if self.locale:
       i18n.setcurrentlocale (self.locale)
 
-    for instance in self.__dirtyInstances.values ():
-      instance.validate ()
+    level = 0
+    todo = self.__dirtyInstances.keys ()
 
+    while todo:
+      # If the maximum number of validation cycles has been reached, we'll stop
+      # here. Looks like some changes in trigger code is needed.
+      if level > self._MAX_CYCLES:
+        raise ValidationCyclesError, \
+            [i.getClassname () for i in self.__dirtyInstances.values ()]
+
+      # Now validate all instances of the current level, and remove them one by
+      # one from the list of dirty instances. This way we do not produce
+      # additional cycles if instances are changing themselfs
+      for objectId in todo:
+        instance = self.__dirtyInstances [objectId]
+        instance.validate ()
+        del self.__dirtyInstances [objectId]
+
+      # All remaining instances in the sequence are forming the next validation
+      # cycle
+      todo = self.__dirtyInstances.keys ()
+      level += 1
+
+    # If no validation raised an exception and all cycles are passed
+    # successfully we can send a commit to the backend now.
     self.__connection.commit ()
-    self.__dirtyInstances = {}
 
 
   # ---------------------------------------------------------------------------





reply via email to

[Prev in Thread] Current Thread [Next in Thread]