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

From: Andrus Adamchik (andru..bjectstyle.org)
Date: Fri May 05 2006 - 11:48:18 EDT

  • Next message: Jean T. Anderson: "Re: EmbeddedDriver performance help"

    Cayenne optimistic locking is "fail on commit". If I understand
    correctly what Eric is adding here is the ability to notify the users
    that "some data has just changed" even before the commit occurs
    instead of silently merging the changes. This can probably be useful
    under some circumstances.

    Andrus

    On May 4, 2006, at 2:23 PM, Bryan Lewis wrote:

    > 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 : Fri May 05 2006 - 11:48:44 EDT