cayenne-user mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Eric Lazarus <ericllaza...@yahoo.com>
Subject Correctly implementing optimistic concurrency management in Cayenne for web-based computing
Date Thu, 04 May 2006 17:11:50 GMT
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 

Mime
View raw message