Correctly implementing optimistic concurrency management in Cayenne for web-based computing

From: Eric Lazarus (ericllazaru..ahoo.com)
Date: Thu May 04 2006 - 13:11:50 EDT

  • Next message: Bryan Lewis: "Re: Correctly implementing optimistic concurrency management in Cayenne for web-based computing"

    Anyone want to write an article on: Correctly
    implementing optimistic concurrency management in
    Cayenne for web-based computing? We will soon have a
    commercial application doing this! I would think it
    would be something that lots of web-developers should
    want to do if they are building a web-based
    application with a domain object model.

    Anyway, we still have a few small problems with it.
    We obtain a set of ObjectIds for all objects changed
    in our session, and another set of ObjectIds for all
    objects changed in other sessions. When these sets
    have a non-empty intersection we have an update
    conflict
    and rollback our changes, and issue a message
    containing the offending objects.

    The Ids changed in our session come from
    DataContext.modifiedObjects()
    The Ids changed in other sessions come from a set
    maintained by our
    DataContextDelegate, which adds the ids of objects
    passed to its
    "shouldMergeChanges() method. This set is cleared each
    time we commit
    or rollback our changes.

    Perhaps we should shorten the period of time when
    conflicts can occur by clearing the changed-elsewhere
    set at the beginning of the page submit cycle rather
    than at the end of the pervious submit. How can we
    tell if that will give us the correct semantics? We
    want to make sure that we are not overwriting new data
    with
    old data. Would we be messing that up if we clear he
    changed-elsewhere set just before
    we begin updating objects in the object model?

    What is most troubling for us right now is that we are
    hitting conflicts on objects that we are not
    intentionally modifying, and are having trouble
    tracking down the code that is causing these objects
    to wind up in the "modified"
    collections.

    All of our persistent objects descend from the class
    PAXPersistentObject, which extends CayenneDataObject.
    I used this to try to track down the
    updates by overriding writeProperty(),
    addToManyTarget(), removeToManyTarget(),
    setToOneDependentTarget(), and setPersistenceState()
    produce a log of ObjectIds
    and their modified fields (or PersistenceState).

    Am I logging in on the correct methods? What else
    might be called that would cause an object to be put
    into modifiedObjects and/or passed to the data context
    delegate?

    The problem is that objects are winding up causing
    update conflicts without showing up in this log.
    Strangely we never see setPersistanceState set the
    state to 'Modified' - the only values we see are
    Hollow (5) and Committed (3).

    Is there some better way to find which fields and
    records in our database are
    being modified ? It would be great if we could find a
    spot to put a breakpoint
    where a stack trace could lead to the code that is
    doing the modification, and
    where the objectid and property name are availible so
    that we can filter out the many fields that we are not
    conserned with.

    To summarize we have 2 questions.
    (1) What is the best way to find places in our code
    that
         are causing un-intended updates?
    (2) Are we constructing the update conflict set
    correctly?

    Below you will find the relevent portions of our code.
    Can you help ?

    If notice we are missusing Cayenne in some way, please
    let us know. We love it and want to use it right.

    Dan and Eric

    /* This is called after all updates are complete, just
    before the page
       is rendered. It will rollback when an update
    conflict is found, and
       commit otherwise.
                 */

    public void preRenderNotification(EditorContext
    aContext) {
      try {
        Set aSet= getUpdateConflictSet(aContext) ;
        if(aSet.size()>0) rollbackChanges(aContext,aSet) ;
        else getDataContext(aContext).commitChanges() ;
      } catch(Exception e) {
        getAppModel(aContext).addMessage(e, "Error In
    Database Update") ;
        e.printStackTrace() ;
    getDataContext(aContext).rollbackChanges() ;
      }
    }

    /* this calculates the conflict set as the
    intersection of
       the 'changed here' and the 'changed elsewhere' sets
     */

    public static Set getUpdateConflictSet(EditorContext
    aContext) {
      Collection
    modified_Objects=getDataContext(aContext).modifiedObjects();
      Set changedHereIDs =
    getIdSetFromObjectCollection(modified_Objects);
      Set changedOtherIDs=
    getAChangeElseWhereSet(aContext);
      changedOtherIDs.retainAll(changedHereIDs);
      return changedOtherIDs;
    }

    /* This just gets the object ids from a collection of
    data objects */

    public static Set
    getIdSetFromObjectCollection(Collection
    modifiedObjects) {
      Set aSet=new HashSet();
      Object anArray[]=modifiedObjects.toArray();
      for(int i=0;i<anArray.length;i++) {
        DataObject aDataObject=(DataObject)anArray[i] ;
        aSet.add(aDataObject.getObjectId()) ;
      }
      return aSet;
    }

    /* This does the rollback, and outputs the error
    message */

    public void rollbackChanges(EditorContext aContext,
    Set aChangeElseWhereSet)
    {
      System.out.println("%%%%%%%%%%%%% Can not Commit
         
    %%%%%%%%%%%%%%%%");
      getDataContext(aContext).rollbackChanges();
      AppModel
    anAppModel=(AppModel)aContext.getModelValue() ;
      Iterator i=aChangeElseWhereSet.iterator();
      while(i.hasNext())
    anAppModel.addMessage("Conflicting object
    id="+i.next()) ;
      aChangeElseWhereSet.clear();
    }

    /* this gets the 'ChangeElsewhere' set that is shared
       with the DataContextDelegate out of session state
    */

    protected static HashSet
    getAChangeElseWhereSet(EditorContext
    aRootEditorContext) {
      HttpServletRequest aHttpServletRequest =
    (HttpServletRequest)
    aRootEditorContext.getHttpServletRequest();
      HashSet aChangedElseWhereSet ; HttpSession session
    ;
      try {
       session = (HttpSession)
    aHttpServletRequest.getSession();
      } catch (IllegalStateException ise) {
       return null;
      }

     
    aChangedElseWhereSet=(HashSet)session.getAttribute("ChangedElseWhere");
      if(aChangedElseWhereSet==null) {
        aChangedElseWhereSet=new HashSet();
        aRootEditorContext.set("session:ChangedElseWhere",

    aChangedElseWhereSet);
        aChangedElseWhereSet = (HashSet)
    session.getAttribute("ChangedElseWhere");
      }
      return aChangedElseWhereSet;
    }

    /* This retrievs the DataContext from session state.
       But on first call it creates the
    DataContextDelegate and passes
       the 'changed elsewhere' set from session state for
    it to fill */

    public static DataContext getDataContext(EditorContext
    aRootEditorContext) {
      HttpServletRequest aHttpServletRequest =
    (HttpServletRequest)
    aRootEditorContext.getHttpServletRequest();
      DataContext aDataContext ; HttpSession session ;
      try {
       session = (HttpSession)
    aHttpServletRequest.getSession();
      } catch (IllegalStateException ise) {
       System.out.println("Exceptions If session is
    null"+ise.getMessage());
       return null;
      }

      aDataContext = (DataContext)
    session.getAttribute("CayenneDataContext");
      if (aDataContext == null) {
        aDataContext = DataContext.createDataContext();
       
    aRootEditorContext.set("session:CayenneDataContext",
    aDataContext);
        aDataContext = (DataContext)
    session.getAttribute("CayenneDataContext");

        OpDataContextDelegate aDelegate= new
    OpDataContextDelegate(getAChangeElseWhereSet(aRootEditorContext));
        aDataContext.setDelegate(aDelegate) ;
      }
      return aDataContext;
    }

    /* This is the 'shouldMergeChanges' method from the
    DataContextDelegate */

    public boolean shouldMergeChanges(DataObject arg0,
    DataRow arg1) {
      getAChangedElseWhereSet().add( arg0.getObjectId() )
    ;
      return true ;
    }

    -------------------------------------------------------------------------------
    -------------------------------------------------------------------------------

    /* This is the PAXPersistant Object where we log
    updates to try to find
       which fields are causing objects to be marked
    'modified' */

    public class PAXPersistentObject extends
    org.objectstyle.cayenne.CayenneDataObject {

      public void writeProperty(String key, Object value)
    {
        filterAndLog(" [*"+key+"*]") ;
        super.writeProperty(key, value) ;
      }

      public void addToManyTarget(String key,
    CayenneDataObject obj, boolean
    bool) {
         filterAndLog(" [+"+key+"+]") ;
         super.addToManyTarget(key, obj, bool) ;
      }

      public void removerFromManyTarget(String key,
    CayenneDataObject obj,
    boolean bool) {
         filterAndLog(" [+"+key+"+]") ;
         super.removeToManyTarget(key, obj, bool) ;
      }

      public void setPersistenceState(int n ) {
         filterAndLog(" {>"+n+"<}") ;
         super.setPersistenceState(n) ;
      }

      private void filterAndLog(String tag) {
        String name=this.getClass().getName() ;
        if (name.startsWith("demo."))
    name=name.substring(5) ;
        if (omit.contains(name)) return ;
        debuglog(pad(name)+" "+tag) ;
      }

      private String pad(String text) {
        if (text.length()>blanks.length()) return text ;
        return (text+blanks).substring(0, blanks.length())
    ;
      }

      private static PrintWriter out ;
      private static String blanks="
           " ;
      private static SimpleDateFormat df=new
    SimpleDateFormat("MM/dd HH:mm:ss")
    ;
      private static String[] omits={"Deal", "Seller",
    "Address", "Quote",
    "DoListItem", "DoListOption", "DoListOptionVar",
    "LogEntry", "CashFlow",
    "CashFlowForSplit"} ;
      private static List omit=Arrays.asList(omits) ;

      public static void debuglog(String text) {
        if (text==null) {
          if ( out !=null ) {
            out.println("************CLOSE**** "+df.format(new
    Date())) ;
            out.flush() ; out.close() ; out=null ;
          }
        } else {
          if (out==null) {
            String path="DebugLog.txt" ;
            try { out= new PrintWriter(new FileWriter(path,
    true)) ;}
            catch (IOException x) { throw MessageToUser.error(x,
    "Error Opening Debug
    Log") ;}
            out.println("************OPEN***** "+df.format(new
    Date()) ) ; out.flush()
    ;
          }
          out.println(text) ; out.flush() ;
        }
      }
    }

    __________________________________________________
    Do You Yahoo!?
    Tired of spam? Yahoo! Mail has the best spam protection around
    http://mail.yahoo.com



    This archive was generated by hypermail 2.0.0 : Thu May 04 2006 - 13:12:15 EDT