[Top][All Lists]
[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 = {}
# ---------------------------------------------------------------------------
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [gnue] r6876 - trunk/gnue-appserver/src,
johannes <=