[ModeShape 5.x] Dealing with aborted user transactions
illia.khokholkov Feb 24, 2017 3:40 PMI am seeking for a guidance on how to deal with aborted user transactions. I am utilizing Arjuna as a transaction manager and make use of user transactions when multiple workspace-write JCR methods are necessary to complete the requested task. By default, the transaction manager in use has a timeout of 60 seconds, which is plenty, and it does allow for custom timeout configuration. I was testing how ModeShape would behave if transaction reaper that comes with Arjuna aborts an active transaction because of a timeout. I do not fully understand whether what I see is what is expected to happen. Hence, please look at the examples provided below. Any help is greatly appreciated. Please, note that the links to the test source code are provided in the end of this post (the examples do not reflect the nature of the problem I am working on, they simply attempt to reproduce the outcome that I see).
The example of adding a single child node to a given parent node inside a user transaction. For the full source code, please refer to [1].
- Lock the node to be updated by obtaining shallow, open-scoped JCR lock.
- Start user transaction, which is set to time out after 3 seconds.
- Pause the thread of execution for 5 seconds to give transaction reaper a reason to abort the current transaction.
- Add a child node to the existing one.
- Save the session.
- End user transaction.
- Unlock the initially locked node.
- Check whether the new child node was added to the repository.
@Test public void addOneNode() throws Exception { Session session = createSession(repositoryIterator.next()); MutableObject<String> childPath1 = new MutableObject<>(); try { Node parentNode = session.getNode(ABSOLUTE_PARENT_NODE_PATH); NodeHelper.lockNode(parentNode); try { TransactionExecutor.runInTransaction(() -> { try { Thread.sleep(TimeUnit.SECONDS.toMillis(5)); } catch (InterruptedException e) { throw new RuntimeException(e); } Node childNode1 = parentNode.addNode(UUID.randomUUID().toString()); childNode1.addMixin("mix:lockable"); session.save(); childPath1.setValue(childNode1.getPath()); return null; }); } finally { NodeHelper.unlockNode(parentNode); } } finally { session.logout(); } assertThat(childPath1.getValue()).isNotNull(); Session newSession = createSession(repositoryIterator.next()); try { assertThat(newSession.nodeExists(childPath1.getValue())) .as("The child node should not be saved, because user transaction was aborted") .isFalse(); } finally { newSession.logout(); } }
To my surprise, despite the fact that user transaction was aborted, ModeShape created a brand new transaction to fulfill the request to save the session and the new child node got persisted in the data store. Looking at the underlying source code, what I observed matches the existing algorithm. I assume that this behavior is intentional, because there was no way for ModeShape to detect that user transaction got aborted, therefore, it had nothing else to do other than create a new transaction to take care of the request to save the changes. Is this correct?
The example of adding two child nodes to a given parent node inside a user transaction. For the full source code, please refer to [2].
- Lock the node to be updated by obtaining shallow, open-scoped JCR lock.
- Start user transaction, which is set to time out after 3 seconds.
- Add the first child node.
- Save the session.
- Pause the thread of execution for 5 seconds to make sure that transaction reaper does terminate the initial user transaction.
- Add the second child node.
- Attempt to save the session.
- End user transaction.
- Unlock the initially locked node.
- Attempt to check whether any of the child nodes were added.
@Test public void addTwoNodes() throws Exception { Session session = createSession(repositoryIterator.next()); MutableObject<String> childPath1 = new MutableObject<>(); MutableObject<String> childPath2 = new MutableObject<>(); try { Node parentNode = session.getNode(ABSOLUTE_PARENT_NODE_PATH); NodeHelper.lockNode(parentNode); try { TransactionExecutor.runInTransaction(() -> { Node childNode1 = parentNode.addNode(UUID.randomUUID().toString()); childNode1.addMixin("mix:lockable"); session.save(); try { Thread.sleep(TimeUnit.SECONDS.toMillis(5)); } catch (InterruptedException e) { throw new RuntimeException(e); } Node childNode2 = parentNode.addNode(UUID.randomUUID().toString()); childNode2.addMixin("mix:lockable"); session.save(); childPath1.setValue(childNode1.getPath()); childPath2.setValue(childNode2.getPath()); return null; }); } finally { NodeHelper.unlockNode(parentNode); } } finally { session.logout(); } // This part of the test is never reached, due to timeout exception on the attempt to save // the second added node. assertThat(childPath1.getValue()).isNotNull(); assertThat(childPath2.getValue()).isNotNull(); Session newSession = createSession(repositoryIterator.next()); try { assertThat(newSession.nodeExists(childPath1.getValue())) .as("The first child node should not be saved, because user transaction was aborted") .isFalse(); assertThat(newSession.nodeExists(childPath2.getValue())) .as("The seconds child node should not be saved, because user transaction was aborted") .isFalse(); } finally { newSession.logout(); } }
An attempt to save the session after adding a second child node takes a while and then errors with:
Caused by: org.modeshape.jcr.TimeoutException: Timeout while attempting to lock the keys [4a789507505d642b9564ce-24e3-49d5-b8a7-b1cc36785ae4] after 0 retry attempts. at org.modeshape.jcr.cache.document.WritableSessionCache.lockNodes(WritableSessionCache.java:1543) at org.modeshape.jcr.cache.document.WritableSessionCache.save(WritableSessionCache.java:687) ... 34 more
If nothing else, I would expect ModeShape to successfully create a transaction and persist the requested changes, based on what was done in the first example, where changes were persisted despite the interrupted and disassociated user transaction. Is this expected and if so, why? If atomicity of actions executed within a user transaction cannot be enforced when user transaction gets aborted and ModeShape creates a new one, does it even make sense to set transaction timeout (should it simply be indefinite to avoid persisting changes that should not be)? Am I misusing something or making false assumptions about how Arjuna and ModeShape should behave? Does ModeShape support nested transactions? Many thanks in advance, your help is greatly appreciated.
[1] modeshape-cluster-test/TransactionTest.java at master · dnillia/modeshape-cluster-test · GitHub
[2] modeshape-cluster-test/TransactionTest.java at master · dnillia/modeshape-cluster-test · GitHub