commit-gnue
[Top][All Lists]
Advanced

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

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


From: johannes
Subject: [gnue] r7308 - trunk/gnue-appserver/src
Date: Thu, 7 Apr 2005 04:31:58 -0500 (CDT)

Author: johannes
Date: 2005-04-07 04:31:56 -0500 (Thu, 07 Apr 2005)
New Revision: 7308

Modified:
   trunk/gnue-appserver/src/data.py
   trunk/gnue-appserver/src/geasInstance.py
   trunk/gnue-appserver/src/geasSession.py
Log:
Improved caching


Modified: trunk/gnue-appserver/src/data.py
===================================================================
--- trunk/gnue-appserver/src/data.py    2005-04-06 23:02:54 UTC (rev 7307)
+++ trunk/gnue-appserver/src/data.py    2005-04-07 09:31:56 UTC (rev 7308)
@@ -26,16 +26,19 @@
 import string
 import copy
 import weakref
+import time
 
 from gnue.common.apps import errors
 from gnue.common.datasources import GDataSource, GConditions, GConnections
 from gnue.common.utils.uuid import UUID
 
+bestone = None
+
 class StateChangeError (errors.SystemError):
-  def __init__ (self, table, row):
-    msg = u_("Changing state from 'commitable' to 'initialized' not allowed "
+  def __init__ (self, table, row, oldState):
+    msg = u_("Changing state from '%(oldstate)s' to 'initialized' not allowed "
              "in table '%(table)s' row %(row)s") \
-          % {'table': table, 'row': row}
+          % {'table': table, 'row': row, 'oldstate': repr (oldState)}
     errors.SystemError.__init__ (self, msg)
 
 class InvalidCacheError (errors.SystemError):
@@ -53,6 +56,13 @@
     msg = u_("Data contains circular references")
     errors.ApplicationError.__init__ (self, msg)
 
+class DuplicateRecordError (errors.SystemError):
+  def __init__ (self, table, row):
+    msg = u_("Duplicate instance '%(row)s' in class '%(table)s'") \
+          % {'table': table, 'row': row}
+    errors.SystemError.__init__ (self, msg)
+
+
 # =============================================================================
 # Cache class
 # =============================================================================
@@ -77,302 +87,453 @@
   # ---------------------------------------------------------------------------
 
   def __init__ (self):
+
     self.__old   = {}                     # Original data
     self.__new   = {}                     # Changed (dirty) data
     self.__state = {}                     # State of the data
 
+    # Atomic cache dictionaries
+    self.__undo     = {}
+    self.__backup   = {}
+
+    # Transaction wide cache dictionaries
+    self.inserted = {}
+    self.deleted  = {}
+
+
   # ---------------------------------------------------------------------------
   # Store data in the cache
   # ---------------------------------------------------------------------------
 
   def write (self, table, row, field, value, dirty):
     """
-    Write data to the cache. If "dirty" is false (0), the cache takes the given
-    value as the original value for the field. If "dirty" is true (1), the
-    value is taken as the modified value for the field, and the original value
-    is remembered (if it was set before).
+    Write data to the clean or the dirty cache. If data is written to the dirty
+    cache and no atomic operation is started already, this function will start
+    a new atomic operation. 
 
-    It is possible to set a dirty value without having set an original value
-    before.
+    A write to the clean cache will be dropped silently if the given field is
+    already available in a 'clean version'. This provides transaction integrity
+    for backends without transaction support. If a clean write succeeds, the
+    state of the given record will be set to 'clean'
+
+    A write to the dirty cache will change the record-state to 'commitable', if
+    no state information was available before or the state was 'clean'.
+    
+    @param table: name of the table
+    @param row: id of the row
+    @param field: name of the field to write data for
+    @param value: the value of the field
+    @param dirty: if True, write operation modifies the dirty cache, otherwise
+        it will change the clean cache.
     """
+
     checktype (table, UnicodeType)
     checktype (row, UnicodeType)
     checktype (field, UnicodeType)
 
-    if dirty:
-      tables = self.__new
-    else:
-      tables = self.__old
+    key = "%s-%s" % (table, row)
 
-    rows   = tables.setdefault (table, {})
-    fields = rows.setdefault (row, {})
+    # If we add 'clean' data there is no need to maintain an undo sequence
+    if not dirty:
+      crow = self.__old.setdefault (table, {}).setdefault (row, {})
+      # But be aware not to overwrite an already cached clean value. This is
+      # necessary to keep transaction integrity for backends without
+      # transaction support.
+      if not crow.has_key (field):
+        crow [field] = value
+        state = self.__state.setdefault (key, 'clean')
 
-    fields [field] = value
+    else:
+      # If there is no backup of the record right now, we have to create one
+      # before applying any changes.
+      if not self.__backup.has_key (key):
+        rec  = self.__new.get (table) and self.__new [table].get (row)
+        self.__backup [key] = (self.__state.get (key), rec and rec.items ())
 
-    # Update state information of the row
-    self.touch (table, row, dirty)
+      self.__new.setdefault (table, {}).setdefault (row, {}) [field] = value
 
+      # After modifying a record update it's state information. If a record is
+      # already initialized or it was a clean record, this modification made it
+      # 'commitable'.
+      state = self.__state.setdefault (key, 'clean')
+      if state in ['initialized', 'clean']:
+        self.__state [key] = 'commitable'
 
 
   # ---------------------------------------------------------------------------
-  # Return whether a certain value is stored in the clean/dirty cache
+  # Read data from the cache
   # ---------------------------------------------------------------------------
 
-  def __has (self, table, row, field, dirty):
+  def read (self, table, row, field, dirty = True):
+    """
+    Read data from the cache. If a dirty value is requested, this function
+    returns the current version of the field, no matter if it's dirty or not.
 
-    if dirty:
+    @param table: name of the table
+    @param row: id of the row
+    @param field: name of the field
+    @param dirty: if True, return the current version of the field, otherwise
+        return only the clean value of the field.
+
+    @raises KeyError: if no value for the given field is available at all
+
+    @return: The current or clean value of the given field
+    """
+
+    checktype (table, UnicodeType)
+    checktype (row, UnicodeType)
+    checktype (field, UnicodeType)
+
+    # Make sure to use the proper dictionary; use the dirty tables only if a
+    # dirty value is available, otherwise always use the clean tables.
+    if dirty and self.__has (table, row, field, True):
       tables = self.__new
     else:
       tables = self.__old
 
-    # We concatenate all has_key () checks so it can return as early as
-    # possible. The function is about 3 times faster than using normal if's
-    return tables.has_key (table) and tables [table].has_key (row) and \
-        tables [table][row].has_key (field)
+    return tables [table] [row] [field]
 
 
   # ---------------------------------------------------------------------------
-  # Return whether a certain value is stored in the cache or not
+  # Return whether a certain value is stored in the clean/dirty cache
   # ---------------------------------------------------------------------------
 
-  def has (self, table, row, field, dirty = None):
+  def __has (self, table, row, field, dirty):
     """
-    Return true (1) if the given item is stored in the cache (either in a clean
-    or in a dirty version). Return false (0) if it isn't.
+    Check wether a given table/row has a clean or dirty version of a given
+    field.
+
+    @param table: the table to be checked
+    @param row: id of the row to be checked
+    @param field: the name of the field to be checked
+    @param dirty: True if the dirty version should be checked, False otherwise
+
+    @return: True if a value for field is available, False otherwise
     """
-    checktype (table, UnicodeType)
-    checktype (row, UnicodeType)
-    checktype (field, UnicodeType)
 
-    if dirty is None:
-      # We could use self.__has () here, but avoiding an additional function
-      # call here is a big deal regarding performance, as this function is
-      # called very often
-      return (self.__old.has_key (table) and self.__old [table].has_key (row) \
-              and self.__old [table][row].has_key (field)) or \
-             (self.__new.has_key (table) and self.__new [table].has_key (row) \
-              and self.__new [table][row].has_key (field))
+    try:
+      (self.__old, self.__new) [dirty] [table] [row] [field]
+
+    except KeyError:
+      return False
+
     else:
-      if dirty:
-        tables = self.__new
-      else:
-        tables = self.__old
-      return tables.has_key (table) and tables [table].has_key (row) and \
-        tables [table][row].has_key (field)
+      return True
 
 
   # ---------------------------------------------------------------------------
-  # Read data from the cache
+  # Return whether a certain value is stored in the cache or not
   # ---------------------------------------------------------------------------
 
-  def read (self, table, row, field, dirty = True):
+  def has (self, table, row, field, dirty = None):
     """
-    Read data from the cache. Depending on the dirty-flag set this function
-    returns the current version, no matter if it's dirty or not. If the
-    dirty-flag is False only the field's old value will be returned.
+    Check wether a given table/row has a clean or dirty version of a given
+    field.
 
-    If the given item isn't available, an exception is raised.
+    @param table: the table to be checked
+    @param row: id of the row to be checked
+    @param field: the name of the field to be checked
+    @param dirty: Could be True, False or None with the following implication:
+        True : search for a 'dirty' version
+        False: search for a 'clean' version
+        None : search for a 'clean' or a 'dirty' version
+
+    @return: True if a value for field is available, False otherwise
     """
+
     checktype (table, UnicodeType)
     checktype (row, UnicodeType)
     checktype (field, UnicodeType)
 
-    if dirty and self.__has (table, row, field, 1):
-      tables = self.__new               # Data is in dirty cache
+    try:
+      # If we do not search for a given kind (clean/dirty) try both
+      if dirty is None:
+        try:
+          self.__old [table] [row] [field]
+
+        except KeyError:
+          self.__new [table] [row] [field]
+
+      elif dirty:
+        self.__new [table] [row] [field]
+
+      else:
+        self.__old [table] [row] [field]
+
+    except KeyError:
+      return False
+
     else:
-      tables = self.__old               # Data isn't in dirty cache, so search
-                                        # in clean cache
-    rows = tables [table]
-    fields = rows [row]
-    return fields [field]
+      return True
 
 
   # ---------------------------------------------------------------------------
-  # Get the status of a record
+  # Update the state information of a given row
   # ---------------------------------------------------------------------------
 
-  def status (self, table, row):
+  def initialized (self, table, row):
     """
-    Returns the status of the given row. Returns one of the following results:
+    Finish initialization of a given record (table/row). The next call of the
+    write () method will change the state to 'commitable'.
 
-    'initializing': newly created record with initialization not yet finished
+    @param table: name of table
+    @param row: name of the row
 
-    'initialized': newly created and initialized records with no modifications
+    @raises StateChangeError: if the former state is not 'initializing'.
+    """
 
-    'inserted': newly created record with modifications
-
-    'changed': existing record with modifications
-
-    'deleted': deleted record
-
-    For this function to work, an original value for the 'gnue_id' field must
-    be available for any record except for newly created ones, and setting
-    'gnue_id' to None means deleting the record.
-    """
     checktype (table, UnicodeType)
     checktype (row, UnicodeType)
 
-    if not self.__has (table, row, u'gnue_id', 0):
-      return ''                         # row is not in cache at all
+    key = "%s-%s" % (table, row)
 
-    old_id = self.__old [table] [row] [u'gnue_id']
+    # Calling initialized () is allowed only if the former state was
+    # 'initializing'. Otherwise it must be a bug !
+    state = self.__state.get (key)
+    if state != 'initializing':
+      raise StateChangeError, (table, row, state)
 
-    if self.__has (table, row, u'gnue_id', 1):
-      new_id = self.__new [table] [row] [u'gnue_id']
-    else:
-      new_id = old_id
+    self.__state [key] = 'initialized'
 
-    if old_id is None:
-      if new_id is None:
-        return ''                       # row was inserted and deleted
-      else:
-        rowState = self.__getState (table, row)
-        if rowState == 'commitable':
-          return 'inserted'
-        else:
-          return rowState
-    else:
-      if new_id is None:
-        return 'deleted'
-      else:
-        if self.__new.has_key (table):
-          rows = self.__new [table]
-          if rows.has_key (row):
-            return 'changed'
-        return ''                       # row has no dirty fields
 
 
   # ---------------------------------------------------------------------------
-  # List all tables with dirty records
+  # Insert a new record
   # ---------------------------------------------------------------------------
 
-  def dirtyTables (self):
+  def insertRecord (self, table, row):
     """
-    Returns a dictionary of tables with dirty data (inserted, changed or
-    deleted rows), where the key is the table name and the value is a
-    dictionary of all dirty rows in the table, where the key is the row id and
-    the value is a dictionary of all dirty fields in that row, where the key is
-    the field name and the value is the current value of the field. Got it?
+    Insert a new record to the cache. This adds the field 'gnue_id' to the
+    dirty cache and sets the record-state to 'initializing'. Make sure to call
+    'initialized ()' in order to make a record 'commitable' by further writes.
+
+    @param table: name of the table
+    @param row: id of the new record
+
+    @raises DuplicateRecordError: If a record (table, row) should be inserted
+         for which we have state information alredy!
     """
 
-    return self.__new
+    key = "%s-%s" % (table, row)
 
+    # There must not be state information for the new record yet!
+    if key in self.__state:
+      raise DuplicateRecordError, (table, row)
+
+    # A new record always starts with a state of 'initializing', which means 
+    # one has to call the initialized () method in order to let further changes
+    # bring the record into a 'commitable' state.
+    self.__state [key] = 'initializing'
+
+    # Create a backup of (None, None) for the record and initialize state. This
+    # also checks the type of table and row arguments.
+    self.write (table, row, u'gnue_id', row, True)
+
+    # Mark this record as 'inserted' and add it to the undo-sequence
+    self.inserted [key] = (table, row)
+    self.__undo [key] = 'insert'
+
+
   # ---------------------------------------------------------------------------
-  # Clear the whole cache
+  # Delete a record from the cache
   # ---------------------------------------------------------------------------
 
-  def clear (self, oldOnly = False):
+  def deleteRecord (self, table, row):
     """
-    Forget all data in the cache, original values as well as dirty values.
+    Delete a given record from the cache. The state of the given record changes
+    to 'deleted' and the record will be added to the dirty tables.
+
+    @param table: name of the table
+    @param row: id of the row
     """
 
-    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]
+    key = "%s-%s" % (table, row)
+    if not self.__backup.has_key (key):
+      rec  = self.__new.get (table) and self.__new [table].get (row)
+      self.__backup [key] = (self.__state.get (key), rec and rec.items ())
 
-        # if a table has no more rows, remove it too
-        if not self.__old [table].keys ():
-          del self.__old [table]
+    # If the record is added within the same transaction, drop it from the
+    # insertion queue, but make sure to keep a note regarding it's state within
+    # the atomic operation.
+    if self.inserted.has_key (key):
+      # If the record has been created within the same atomic operation, it's
+      # safe to remove it from the insertion queue. This is because a cancel
+      # does not restore that inserted record. If the record has been created
+      # by another atomic operation, we have to re-add it to the inserted list
+      # on canceling this deletion.
+      if not self.__undo.has_key (key):
+        self.__undo [key] = 'remove'
+          
+      del self.inserted [key]
 
+    # The record is not added within the same transaction, so just add it to
+    # the deletion queue
     else:
-      self.__old   = {}
-      self.__new   = {}
-      self.__state = {}
+      self.deleted [key] = (table, row)
+      self.__undo [key]  = 'delete'
 
+    # Finally remove the current 'gnue_id' and set the record state to
+    # 'deleted'. The former will add this record to the dirty tables, if it was
+    # clean before.
+    self.write (table, row, u'gnue_id', None, True)
+    self.__state [key] = 'deleted'
 
+
   # ---------------------------------------------------------------------------
-  # Update the state information of a given row
+  # Confirm changes
   # ---------------------------------------------------------------------------
 
-  def initialized (self, table, row):
+  def confirm (self):
     """
-    This function sets a row of a table to 'initialized'. This means the next
-    write () to this table makes this row 'commitable'
+    Close the current atomic operation by confirming all it's changes.
     """
 
-    checktype (table, UnicodeType)
-    checktype (row, UnicodeType)
+    self.__undo.clear ()
+    self.__backup.clear ()
 
-    cState = self.__getState (table, row)
-    if cState is not None and cState == 'commitable':
-      raise StateChangeError, (table, row)
 
-    self.__setState (table, row, 'initialized')
-
-
   # ---------------------------------------------------------------------------
-  # Create state information for a given table/row
+  # Cancel atomic operation
   # ---------------------------------------------------------------------------
 
-  def __setState (self, table, row, state):
+  def cancel (self):
     """
+    Cancel the current atomic operation by revoking all it's changes.
     """
 
-    checktype (table, UnicodeType)
-    checktype (row, UnicodeType)
+    while self.__backup:
+      (key, (state, items)) = self.__backup.popitem ()
 
-    if not self.__state.has_key (table):
-      self.__state [table] = {}
+      # Restore state information
+      if state is not None:
+        self.__state [key] = state
 
-    rows = self.__state [table]
-    rows [row] = state
+      elif self.__state.has_key (key):
+        del self.__state [key]
 
+      # Restore current (dirty) record data
+      table, row = key.split ('-')
+      if items is not None:
+        rec = self.__new.setdefault (table, {}).setdefault (row, {})
+        # Remove all fields from the row-dictionary and add all previously
+        # stored (field, value) tuples
+        rec.clear ()
+        for (field, value) in items:
+          rec [field] = value
 
+      elif items is None:
+        # No changed data was available before, so remove it from the cache
+        self.__removeFromDict (self.__new, table, row)
+
+      # If we have an entry in the current atomic cache, do further processing
+      if self.__undo.has_key (key):
+        action = self.__undo.pop (key)
+
+        try:
+          # Remove the record from the insertion queue
+          if action == 'insert':
+            del self.inserted [key]
+
+          # Re-Add the previously deleted record to the insertion queue
+          elif action == 'remove':
+            self.inserted [key] = (table, row)
+
+          # Remove the record from the deletion queue
+          elif action == 'delete':
+            del self.deleted [key]
+
+        except KeyError:
+          pass
+            
+    assert not self.__undo, "atomic operation cache *not* empty"
+    assert not self.__backup, "atomic record cache *not* empty"
+
+
   # ---------------------------------------------------------------------------
-  # Return the current state of a row 
+  # Get the state of a record
   # ---------------------------------------------------------------------------
 
-  def __getState (self, table, row):
+  def state (self, table, row):
     """
-    This function returns the current state of a row
-    @param table: name of the table to get state information for
-    @param row: gnue-id of the row to get state information for
+    Returns the state of the given record. Returns one of the following 
results:
 
-    @return: state information for the requested row
+      'initializing': newly created record with initialization not yet finished
+      'initialized': newly created and initialized records with no 
modifications
+      'inserted': newly created record with modifications
+      'changed': existing record with modifications
+      'deleted': deleted record
+      'clean': record is available and has no modifications
+
+    @return: current state of the given (table, row) tuple in the cache
     """
 
     checktype (table, UnicodeType)
     checktype (row, UnicodeType)
 
-    if self.__state.has_key (table) and self.__state [table].has_key (row):
-      return self.__state [table] [row]
+    key   = "%s-%s" % (table, row)
+    state = self.__state.get (key, 'clean')
 
+    if self.inserted.has_key (key):
+      return (state, 'inserted') [state == 'commitable']
+
+    elif self.deleted.has_key (key):
+      return 'deleted'
+
     else:
-      return None
+      return (state, 'changed') [state == 'commitable']
 
 
   # ---------------------------------------------------------------------------
-  # Touch a record in the cache
+  # List all tables with dirty records
   # ---------------------------------------------------------------------------
 
-  def touch (self, table, row, dirty = True):
+  def dirtyTables (self):
     """
-    This function touches a record in the cache. If has no state information it
-    will be stet to 'initializing' and if the dirty flag is set and state is
-    already 'initialized' the record get's 'commitable'
+    Returns a dictionary of tables with dirty data (inserted, changed or
+    deleted rows), where the key is the table name and the value is a
+    dictionary of all dirty rows in the table, where the key is the row id and
+    the value is a dictionary of all dirty fields in that row, where the key is
+    the field name and the value is the current value of the field. Got it?
+    """
 
-    @param table: name of the table to be touched
-    @param row: gnue_id of the row to be touched
-    @param dirty: boolean flag to state wether to make the record dirty or not
+    return self.__new
+
+
+  # ---------------------------------------------------------------------------
+  # Clear the whole cache
+  # ---------------------------------------------------------------------------
+
+  def clear (self, oldOnly = False):
     """
+    Clear cached data, either all data or only the clean portion.
 
-    # Update state information of the row
-    state = self.__getState (table, row)
+    @param oldOnly: if True, only the clean portion of the cache will be
+        cleaned. This will be used to implement dirty reads.
+    """
 
-    if state is None:
-      state = 'initializing'
-      self.__setState (table, row, 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 ():
+          # remove clean or deleted rows
+          if self.state (table, row) in ['clean', 'deleted']:
+            del self.__old [table][row]
+            key = "%s-%s" % (table, row)
+            if self.__state.has_key (key):
+              del self.__state [key]
 
-    if dirty and state == 'initialized':
-      self.__setState (table, row, 'commitable')
+        # if a table has no more rows, remove it too
+        if not self.__old [table]:
+          del self.__old [table]
 
+    else:
+      self.__old.clear ()
+      self.__new.clear ()
+      self.__state.clear ()
 
+
+
   # ---------------------------------------------------------------------------
   # Make the given row in a table to be treated as 'clean'
   # ---------------------------------------------------------------------------
@@ -386,16 +547,23 @@
     @param row: gnue_id of the row to be moved
     """
 
+    key = "%s-%s" % (table, row)
+
     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.setdefault (table, {}) [row] = self.__new [table] [row]
 
-      self.__old [table] [row] = self.__new [table] [row]
-
     self.__removeFromDict (self.__new, table, row)
-    self.__removeFromDict (self.__state, table, row)
 
+    if self.__state.has_key (key):
+      del self.__state [key]
 
+    if key in self.inserted:
+      del self.inserted [key]
+
+    elif key in self.deleted:
+      del self.deleted [key]
+
+
   # ---------------------------------------------------------------------------
   # Remove a row of a table completely from the cache
   # ---------------------------------------------------------------------------
@@ -406,14 +574,21 @@
     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
+    @param row: 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)
 
+    key = "%s-%s" % (table, row)
 
+    if key in self.__state:
+      del self.__state [key]
+
+    if key in self.deleted:
+      del self.deleted [key]
+
+
   # ---------------------------------------------------------------------------
   # Remove a row of a table from a given cache-dictionary
   # ---------------------------------------------------------------------------
@@ -426,34 +601,48 @@
 
     @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
+    @param row: id of the row to be removed
     """
 
-    if dictionary.has_key (table) and dictionary [table].has_key (row):
-      del dictionary [table] [row]
+    try:
+      if row in dictionary [table]:
+        del dictionary [table] [row]
 
-    if dictionary.has_key (table) and not len (dictionary [table].keys ()):
-      del dictionary [table]
+      if not dictionary [table]:
+        del dictionary [table]
 
+    except KeyError:
+      pass
 
+
   # ---------------------------------------------------------------------------
-  # Have a look if the cache is really in a clean state
+  # Get a sequence of fields not yet kept in any cache
   # ---------------------------------------------------------------------------
 
-  def _assertClean (self):
+  def uncachedFields (self, table, row, fields):
     """
-    This function iterates over all 'new' records in the cache and verifies if
-    they're in a clean state.
+    Return a sequence of fields which are not already cached in the clean or
+    dirty cache. 
+
+    @param table: name of the table
+    @param row: id of the row
+    @param fields: sequence of fields to be checked
+
+    @return: subset from fields, which do not have a clean or dirty value
+        cached at the moment
     """
 
-    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))
+    result = []
+    append = result.append
 
-    
+    for item in fields:
+      if not self.has (table, row, item):
+        append (item)
 
+    return result
 
+
+
 # =============================================================================
 # Helper methods
 # =============================================================================
@@ -564,12 +753,7 @@
     self.__connections = connections
     self.__database    = database
     self.__cache       = _cache ()
-    self.__inserted    = []
-    self.__deleted     = []
 
-    self.__confirmedCache   = None
-    self.__confirmedInserts = []
-    self.__confirmedDeletes = []
     self.__constraints = {}
 
     self.__uuidType = gConfig ('uuidtype').lower ()
@@ -634,6 +818,11 @@
       checktype (fields, ListType)
       for fields_element in fields: checktype (fields_element, UnicodeType)
 
+    checktype (order, [NoneType, ListType])
+    if order:
+      for order_element in order:
+        checktype (order_element, DictType)
+
     return recordset (self.__cache, self.__connections, self.__database,
                       content, GConditions.buildCondition (conditions), order)
 
@@ -648,20 +837,19 @@
 
     Table must be a unicode string.
     """
+
     checktype (table, UnicodeType)
 
     if self.__uuidType == 'time':
-      id = UUID.generateTimeBased ()
+      row = UUID.generateTimeBased ()
     else:
-      id = UUID.generateRandom ()
+      row = UUID.generateRandom ()
 
-    r = record (self.__cache, self.__connections, self.__database, table, id)
-    self.__cache.write (table, id, u'gnue_id', None, 0)  # old id is None
-    self.__cache.write (table, id, u'gnue_id', id, 1)    # new id
-
-    self.__inserted.append ((table, id))
+    self.__cache.insertRecord (table, row)
+    r = record (self.__cache, self.__connections, self.__database, table, row)
     return r
 
+
   # ---------------------------------------------------------------------------
   # Delete a record
   # ---------------------------------------------------------------------------
@@ -674,14 +862,13 @@
 
     Table and row must be unicode strings.
     """
+
     checktype (table, UnicodeType)
     checktype (row, UnicodeType)
 
-    if not self.__cache.has (table, row, u'gnue_id'):    # not yet in cache
-      self.__cache.write (table, row, u'gnue_id', row, 0)
-    self.__cache.write (table, row, u'gnue_id', None, 1)
-    self.__deleted.append ((table, row))
+    self.__cache.deleteRecord (table, row)
 
+
   # ---------------------------------------------------------------------------
   # Find a record
   # ---------------------------------------------------------------------------
@@ -703,19 +890,22 @@
     for fields_element in fields:
       checktype (fields_element, UnicodeType)
 
-    uncachedFields = []
-    for field in fields:
-      if not self.__cache.has (table, row, field):
-        uncachedFields.append(field)
+    # Let's have a look wether we need a requery or not. Newly inserted records
+    # cannot have uncached fields since they are new :)
+    state = self.__cache.state (table, row)
+    if state in ['initializing', 'initialized', 'inserted']:
+      uncachedFields = []
+    else:
+      uncachedFields = self.__cache.uncachedFields (table, row, fields)
 
-    if uncachedFields == [] or \
-        self.__cache.status (table, row) in ['initializing', 'initialized',
-                                             'inserted']:
-      # already cached, no need to load from database
+    # If there are no uncached fields there is no need to reload from the
+    # backend.
+    if not uncachedFields:
       r = record (self.__cache, self.__connections, self.__database, table, 
row)
-      r._cache (u'gnue_id', row)
+      self.__cache.write (table, row, u'gnue_id', row, dirty = False)
+
+    # otherwise requery the current record for all uncached fields
     else:
-      # not yet cached, need to load from database
       new = self.__backend ().requery (table, {u'gnue_id': row}, 
uncachedFields)
       r = record (self.__cache, self.__connections, self.__database, table, 
row)
       r._fill (None, uncachedFields, new)
@@ -757,17 +947,12 @@
     recNo   = 0
 
     # first perform all inserts
-    if self.__inserted:
+    if self.__cache.inserted:
       for (table, row) in self.__orderInserts ():
         fields = tables [table] [row]
         recNo += 1
 
         backend.insert (table, fields, recNo)
-        self.__inserted.remove ((table, row))
-
-        if (table, row) in self.__confirmedInserts:
-          self.__confirmedInserts.remove ((table, row))
-
         self.__cache.makeClean (table, row)
 
 
@@ -775,26 +960,19 @@
     for (table, rows) in tables.items ():
       for (row, fields) in rows.items ():
         recNo += 1
-        status = self.__cache.status (table, row)
-
-        if status == 'changed':
+        if self.__cache.state (table, row) == 'changed':
           backend.update (table, {'gnue_id': row}, fields, recNo)
           self.__cache.makeClean (table, row)
 
+
     # perform all deletes
-    if len (self.__deleted):
+    if len (self.__cache.deleted):
       for (table, row) in self.__orderDeletes ():
         recNo += 1
         backend.delete (table, {'gnue_id': row}, recNo)
         self.__cache.remove (table, row)
 
 
-    self.__deleted = []
-    self.__confirmedDeletes = []
-
-    # Assert
-    self.__cache._assertClean ()
-
     # Commit the whole transaction
     self.__connections.commitAll ()
 
@@ -815,8 +993,8 @@
     @return: sequence of (table, row) tuples in a sane order for insertion
     """
 
-    records = [(table, row) for (table, row) in self.__inserted \
-                             if self.__cache.status (table, row) == 'inserted']
+    records = [(table, row) for (table, row) in self.__cache.inserted.values 
()\
+                             if self.__cache.state (table, row) == 'inserted']
     result = self.__orderByDependency (records)
     gDebug (1, "Ordered inserts: %s" % result)
 
@@ -835,7 +1013,7 @@
     @return: sequence of (table, row) tuples in a sane order for deletion
     """
 
-    order = self.__orderByDependency (self.__deleted)
+    order = self.__orderByDependency (self.__cache.deleted.values ())
     # since we do deletes we need a reversed order
     order.reverse ()
 
@@ -924,11 +1102,6 @@
     Undo all uncommitted changes.
     """
 
-    self.__inserted = []
-    self.__deleted  = []
-    self.__confirmedInserts = []
-    self.__confirmedDeletes = []
-
     # Send the rollback to the database. Although we have (most probably) not
     # written anything yet, we have to tell the database that a new transaction
     # starts now, so that commits from other sessions become valid now for us
@@ -938,8 +1111,8 @@
     # 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
 
+
   # ---------------------------------------------------------------------------
   # Close the connection
   # ---------------------------------------------------------------------------
@@ -962,12 +1135,7 @@
     cancelChanges () function restores to this state.
     """
 
-    # Doing a deepcopy here is a 'no-go' for performance. If we're creating
-    # larger transaction (with a lot of records) this call will slow down
-    # everything with every record added.
-    self.__confirmedCache   = copy.deepcopy (self.__cache)
-    self.__confirmedInserts = self.__inserted [:]
-    self.__confirmedDeletes = self.__deleted [:]
+    self.__cache.confirm ()
 
 
 
@@ -980,15 +1148,9 @@
     This function revokes all changes up to the last call of the
     confirmChanges () function.
     """
-    if self.__confirmedCache is not None:
-      self.__cache = copy.deepcopy (self.__confirmedCache)
-    else:
-      self.__cache.clear ()
+    self.__cache.cancel ()
 
-    self.__inserted = self.__confirmedInserts [:]
-    self.__deleted  = self.__confirmedDeletes [:]
 
-
 # =============================================================================
 # Recordset class
 # =============================================================================
@@ -1035,7 +1197,7 @@
     """
 
     return self.__resultSet.getRecordCount () + \
-        len (self.__add) + self.__added - len (self.__remove.keys ())
+        len (self.__add) + self.__added - len (self.__remove)
 
 
   # ---------------------------------------------------------------------------
@@ -1057,7 +1219,7 @@
       return rec
 
     else:
-      if not len (self.__order):
+      if not self.__order:
         row = self.__add [0]
         self.__add.remove (row)
         self.__added += 1
@@ -1221,7 +1383,7 @@
     # Iterate over all dirty rows of the master table and stick them into the
     # apropriate filter
     for (row, fields) in tables [master].items ():
-      state = self.__cache.status (master, row)
+      state = self.__cache.state (master, row)
 
       if state == 'inserted':
         # an inserted (new) row must match the current condition
@@ -1248,7 +1410,7 @@
 
     # If we have a sort order defined, we need to sort all records listed for
     # addition
-    if len (order) and len (self.__add):
+    if order and len (self.__add):
       self.__sortAddition (order)
 
 
@@ -1485,6 +1647,8 @@
     """
 
     result = []
+    append = result.append
+
     for element in order:
       field      = element ['name']
       direction  = element.get ('descending') or False
@@ -1494,7 +1658,7 @@
       if ignorecase and hasattr (value, 'lower'):
         value = value.lower ()
 
-      result.append ((value, direction))
+      append ((value, direction))
 
     return result
 
@@ -1515,7 +1679,17 @@
   # ---------------------------------------------------------------------------
 
   def __init__ (self, cache, connections, database, table, row):
+    """
+    Create a new wrapper for a given record in the cache
 
+    @param cache: the underlying cache instance
+    @param connections: GConnections instance which can return a handle to the
+        current backend connection
+    @param database: name of the backend connection to be used
+    @param table: name of the table this record instance is a row of
+    @param row: id of the row represented by this record instance
+    """
+
     self.__cache       = cache
     self.__connections = connections
     self.__database    = database
@@ -1529,6 +1703,10 @@
   # ---------------------------------------------------------------------------
 
   def getRow (self):
+    """
+    Return the current row-id of this record
+    @return: row id of this record instance
+    """
 
     return self.__row
 
@@ -1538,24 +1716,36 @@
   # ---------------------------------------------------------------------------
 
   def _cache (self, field, value):
+    """
+    Write a field to the clean cache, but do not replace already cached values
 
-    if not self.__cache.has (self.__table, self.__row, field):
-      self.__cache.write (self.__table, self.__row, field, value, 0)
+    @param field: name of the field
+    @param value: value of the field
+    """
 
+    self.__cache.write (self.__table, self.__row, field, value, False)
+
+
   # ---------------------------------------------------------------------------
   # Fill the cache for this record with data from a (gnue-common) RecordSet
   # ---------------------------------------------------------------------------
 
   def _fill (self, alias, fields, RecordSet):
+    """
+    Write the given fields from the given RecordSet object to the clean cache,
+    but do not replace already cached values.
 
+    @param alias: alias used for fields in the RecordSet, i.e. 't0', 't1', ...
+    @param fields: sequence of fieldnames to cache from the RecordSet
+    @param RecordSet: dictionary like object to retrieve the data from
+    """
+
     for field in fields:
-      # Never ever override the cache with data from the backend
-      if alias:
-        fn = alias + '.' + field
-      else:
-        fn = field
-      self._cache (field, RecordSet [fn])
+      f = alias and (alias + '.' + field) or field
 
+      self.__cache.write (self.__table, self.__row, field, RecordSet [f], 
False)
+
+
   # ---------------------------------------------------------------------------
   # Get the value for a field
   # ---------------------------------------------------------------------------
@@ -1568,23 +1758,31 @@
     The field name must be given as a unicode string. The result will be
     returned as the native Python datatype, in case of a string field it will
     be Unicode.
+
+    @param field: name of the field to get a value for
+    @param original: if True return an original value (either from the clean
+        cache or freshly fetched from the backend). If False return a dirty or
+        clean value (in that order)
+
+    @return: current or original value of the given field
     """
+
     checktype (field, UnicodeType)
 
-    if original:
-      if self.__cache.has (self.__table, self.__row, field, False):
-        return self.__cache.read (self.__table, self.__row, field, False)
-    else:
-      if self.__cache.has (self.__table, self.__row, field):
-        # If we find the field in the cache, use it
-        return self.__cache.read (self.__table, self.__row, field)
+    # If an original value is requested, set the dirty-flag to False, otherwise
+    # leave it unspecified (=None), which means use a dirty or clean value in
+    # that order.
+    dirty = (None, False) [original]
+    if self.__cache.has (self.__table, self.__row, field, dirty):
+      return self.__cache.read (self.__table, self.__row, field, dirty is None)
 
-    # if state is one of 'in*' the record is new and cannot exist in the
-    # backend.
-    if self.status () in ['initializing', 'initialized', 'inserted']:
+    # if the record is newly inserted, it cannot have values in the backend
+    # until now. So there's no need to fetch anything.
+    if self.state () in ['initializing', 'initialized', 'inserted']:
       return None
 
-    # Not found in cache, so get it from the db
+    # If we have no backend reference until now, create a weak reference to the
+    # connection instance and make sure we're logged in.
     if self.__backend is None:
       cons = self.__connections
       self.__backend = weakref.ref (cons.getConnection (self.__database))
@@ -1594,12 +1792,11 @@
                                      [field])
     value = new.get (field)
     if new:
-      self.__cache.write (self.__table, self.__row, field, value, 0)
+      self.__cache.write (self.__table, self.__row, field, value, False)
 
     return value
 
 
-
   # ---------------------------------------------------------------------------
   # Put the value for a field
   # ---------------------------------------------------------------------------
@@ -1610,10 +1807,14 @@
 
     The field name must be given as a unicode string, value must be the native
     Python datatype of the field, in case of a string field it must be Unicode.
+
+    @param field: name of the field (as unicode string)
+    @param value: value to be set for the given field (native Python datatype)
     """
+
     checktype (field, UnicodeType)
 
-    self.__cache.write (self.__table, self.__row, field, value, 1)
+    self.__cache.write (self.__table, self.__row, field, value, True)
 
 
   # ---------------------------------------------------------------------------
@@ -1624,6 +1825,9 @@
     """
     This function marks this record as 'initialized' so following changes will
     make it commitable.
+
+    @raises StateChangeError: if this function is called for a record having
+        another state than 'initializing'.
     """
 
     self.__cache.initialized (self.__table, self.__row)
@@ -1633,25 +1837,23 @@
   # Get the state of the current record
   # ---------------------------------------------------------------------------
 
-  def status (self):
+  def state (self):
     """
-    This function is a pass-through to the cached record's state
-    """
+    Returns the state of the given record. Returns one of the following 
results:
 
-    return self.__cache.status (self.__table, self.__row)
+      'initializing': newly created record with initialization not yet finished
+      'initialized': newly created and initialized records with no 
modifications
+      'inserted': newly created record with modifications
+      'changed': existing record with modifications
+      'deleted': deleted record
+      'clean': record is available and has no modifications
 
+    @return: current state of the record
+    """
 
-  # ---------------------------------------------------------------------------
-  # Touch a record in the cache
-  # ---------------------------------------------------------------------------
+    return self.__cache.state (self.__table, self.__row)
 
-  def touch (self, dirty = True):
-    """
-    This function touches the underlying record in the cache.
-    """
 
-    self.__cache.touch (self.__table, self.__row, dirty)
-
   # ---------------------------------------------------------------------------
   # Give an official string represenation of the record instance
   # ---------------------------------------------------------------------------

Modified: trunk/gnue-appserver/src/geasInstance.py
===================================================================
--- trunk/gnue-appserver/src/geasInstance.py    2005-04-06 23:02:54 UTC (rev 
7307)
+++ trunk/gnue-appserver/src/geasInstance.py    2005-04-07 09:31:56 UTC (rev 
7308)
@@ -281,7 +281,7 @@
 
     # Do not call OnChange triggers while in OnInit code and when setting
     # time/user stamp fields
-    if regular and self.status () != 'initializing':
+    if regular and self.state () != 'initializing':
       for trigger in self.__classdef.OnChange:
         self.call (trigger, {}, {'oldValue': self.__getValue (propertyname),
                                  'newValue': __value,
@@ -431,43 +431,23 @@
   # Get the state of the instances record in the cache
   # ---------------------------------------------------------------------------
 
-  def status (self):
+  def state (self):
     """
     This function returns the current state of the instance.
 
     @return: state of the instance:
-
-    'initializing': new instance, OnInit still running
-
-    'initialized': new instance, OnInit finished, but no other modifications
-
-    'inserted': new instance, already modified
-
-    'changed': existing instance with modifications
-
-    'deleted': deleted instance
+      'initializing': new instance, OnInit still running
+      'initialized': new instance, OnInit finished, but no other modifications
+      'inserted': new instance, already modified
+      'changed': existing instance with modifications
+      'deleted': deleted instance
+      'clean': existing instance without any modifications
     """
 
-    return self.__record.status ()
+    return self.__record.state ()
 
 
   # ---------------------------------------------------------------------------
-  # Touch the record in the cache
-  # ---------------------------------------------------------------------------
-
-  def touch (self, dirty = True):
-    """
-    This function touches the encapsulated record in the cache. If the record
-    has been initialized already it becomes 'commitable' this way.
-
-    @param dirty: if set to TRUE an 'initialized' record becomes 'commitable',
-        otherwise it might change to 'initializing' only.
-    """
-
-    self.__record.touch (dirty)
-
-
-  # ---------------------------------------------------------------------------
   # Get the fully qualified classname of an instance
   # ---------------------------------------------------------------------------
 

Modified: trunk/gnue-appserver/src/geasSession.py
===================================================================
--- trunk/gnue-appserver/src/geasSession.py     2005-04-06 23:02:54 UTC (rev 
7307)
+++ trunk/gnue-appserver/src/geasSession.py     2005-04-07 09:31:56 UTC (rev 
7308)
@@ -567,10 +567,6 @@
       for object_id in obj_id_list:
         if object_id:
           instance = self.__findInstance (classdef, object_id, [])
-          # make sure an 'initialized' record get's commitable, even if no
-          # further access to the instance occurs
-          instance.touch ()
-
           new_object_id = object_id
 
         else:
@@ -582,7 +578,7 @@
         i += 1
         result.append (new_object_id)
 
-        if instance.status () in ['changed', 'inserted']:
+        if instance.state () in ['changed', 'inserted']:
           instance.updateStamp ()
           self.__dirtyInstances [new_object_id] = instance
 




reply via email to

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