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

From: Bryan Lewis (brya..aine.rr.com)
Date: Thu May 04 2006 - 14:23:17 EDT

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

    I'm missing something. Why not use Cayenne's built-in optimistic
    locking support, with a timestamp or other versioning attribute in each
    important entity?

    Eric Lazarus wrote:

    >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 - 14:23:47 EDT