We heard some users complaining about some save conflicts in one of our XPages application. They got this irritating message:
Document has been saved by another user - Save created a new document as a response to that modified document.
The users have asked all their colleagues but no one had been working on the same document.
Taking a closer look at those documents we found that the conflict document has been created nearly at the same time as the original document had been saved. Both documents had been saved by the same user. The conclusion was obvious. There was something wrong with our code. The strange thing was that the conflict only happened very infrequently, which made searching for the problem a little difficult.
Finally after a hint from Tony McGuckin of the Ireland XPages lab we were able to narrow it down to a timing issue when using the Java classes DominoDocument and Document together.
For demonstration purposes I wrote a test database with a simple Java method.
public void simplifiedSample(DominoDocument uidoc) { try { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:MM:ss"); uidoc.replaceItemValue("Status", "approved"); uidoc.save(); System.out.println(sdf.format(new Date()) + " uidoc.save() " + uidoc.getDocumentId()); java.lang.Thread.sleep(10000); Document backendDoc = uidoc.getDocument(true); Item authorItem = backendDoc.replaceItemValue("CurrentAuthor", "CN=Bernd Hort/O=assono"); authorItem.setAuthors(true); backendDoc.save(); System.out.println(sdf.format(new Date()) + " backendDoc.save() " + backendDoc.getUniversalID()); java.lang.Thread.sleep(10000); uidoc.replaceItemValue("SomeOtherItem", "anotherValue"); uidoc.replaceItemValue("Subject", "Test " + sdf.format(new Date())); uidoc.save(); System.out.println(sdf.format(new Date()) + " uidoc.save() " + uidoc.getDocumentId()); java.lang.Thread.sleep(10000); backendDoc = uidoc.getDocument(true); Item readerItem = backendDoc.replaceItemValue("Supervisor", "CN=Bernd Hort/O=assono"); readerItem.setReaders(true); backendDoc.save(); System.out.println(sdf.format(new Date()) + " backendDoc.save() " + backendDoc.getUniversalID()); } catch (Exception e) { throw new RuntimeException(e); } }
The actual code is way more complex. There are a lot of calls to different methods in business logic classes. Some of them have to use the back end classes Document (corresponding to NotesDocument in LotusScript) because the DominoDocument does not provide the needed methods (like defining an author item).
If I remove the java.lang.Thread.sleep(10000) calls in my simplefiedSample method, I don't get a save conflict. With the Thread.sleep calls there is a save conflict every time.
Looking at the log it is easy to spot where the save conflict occurs.
HTTP JVM: 2013-07-22 15:03:52 uidoc.save() 94E HTTP JVM: 2013-08-22 15:03:02 backendDoc.save() DFB7E2DFD82C80F6C1257B36004D9FAA HTTP JVM: 2013-08-22 15:03:12 uidoc.save() 952 HTTP JVM: 2013-08-22 15:03:22 backendDoc.save() 52EB22DEF8D31AC6C1257B36004DA77D
When the DominoDocument object (uidoc) is saved for the second time there must be some kind of timestamp comparison. The XPages runtime environment discovers the differences and saves the changes as a save conflict. Which by the way would explain why the users only get a error one out of ten times.
A fix for the problem is quite easy. If I skip the second save there is no save conflict.
// uidoc.save(); // System.out.println(sdf.format(new Date()) + " uidoc.save() " // + uidoc.getDocumentId()); System.out.println("skip second uidoc save");
Here is the log for the test without the second uidoc.save():
HTTP JVM: 2013-15-22 15:03:44 uidoc.save() 95E HTTP JVM: 2013-15-22 15:03:54 backendDoc.save() BB088DF6768831FDC1257B36004E5861 HTTP JVM: skip second uidoc save HTTP JVM: 2013-16-22 15:03:14 backendDoc.save() BB088DF6768831FDC1257B36004E5861
Sure the best way would be to work only with the back end classes. This would minimize the risk of any problems.