/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.jcr;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockManager;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionManager;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import org.junit.Ignore;
import org.junit.Test;
import org.modeshape.common.FixFor;
import org.modeshape.common.util.FileUtil;
import org.modeshape.jcr.security.SimplePrincipal;
import org.modeshape.jcr.security.acl.JcrAccessControlList;
import org.modeshape.jcr.security.acl.Privileges;
public class TransactionsTest extends SingleUseAbstractTest {
@FixFor( "MODE-1819" )
@Test
public void shouldBeAbleToMoveNodeWithinUserTransaction() throws Exception {
startTransaction();
moveDocument("childX");
commitTransaction();
}
@FixFor( "MODE-1819" )
@Test
public void shouldBeAbleToMoveNodeOutsideOfUserTransaction() throws Exception {
moveDocument("childX");
}
@FixFor( "MODE-1819" )
@Test
public void shouldBeAbleToUseSeparateSessionsWithinSingleUserTransaction() throws Exception {
// We'll use separate threads, but we want to have them both do something specific at a given time,
// so we'll use a barrier ...
final CyclicBarrier barrier1 = new CyclicBarrier(2);
final CyclicBarrier barrier2 = new CyclicBarrier(2);
// The path at which we expect to find a node ...
final String path = "/childY/grandChildZ";
// Create a runnable to obtain a session and look for a particular node ...
final AtomicReference<Exception> separateThreadException = new AtomicReference<Exception>();
final AtomicReference<Node> separateThreadNode = new AtomicReference<Node>();
Runnable runnable = () -> {
// Wait till we both get to the barrier ...
Session session1 = null;
try {
barrier1.await(20, TimeUnit.SECONDS);
// Create a second session, which should NOT see the persisted-but-not-committed changes ...
session1 = newSession();
Node grandChild2 = session1.getNode(path);
separateThreadNode.set(grandChild2);
} catch (Exception err) {
separateThreadException.set(err);
} finally {
try {
barrier2.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
// Start another session in a separate thread that won't participate in our transaction ...
new Thread(runnable).start();
// Now start a transaction ...
startTransaction();
// Create first session and make some changes ...
Node node = session.getRootNode().addNode("childY");
node.setProperty("foo", "bar");
Node grandChild = node.addNode("grandChildZ");
grandChild.setProperty("foo", "bar");
assertThat(grandChild.getPath(), is(path));
session.save(); // persisted but not committed ...
session.getNode("/childY/grandChildZ").setProperty("bar", "baz");
session.save();
// Use the same session to find the node ...
Node grandChild1 = session.getNode(path);
assertThat(grandChild.isSame(grandChild1), is(true));
assertEquals("bar", grandChild1.getProperty("foo").getString());
assertEquals("baz", grandChild1.getProperty("bar").getString());
// Create a second session, which should see the persisted-but-not-committed changes ...
Session session2 = newSession();
Node grandChild2 = session2.getNode(path);
assertThat(grandChild.isSame(grandChild2), is(true));
assertEquals("bar", grandChild2.getProperty("foo").getString());
assertEquals("baz", grandChild2.getProperty("bar").getString());
session2.logout();
// Sync up with the other thread ...
barrier1.await();
// Await while the other thread does its work and looks for the node ...
barrier2.await(20, TimeUnit.SECONDS);
// Commit the transaction ...
commitTransaction();
// Our other session should not have seen the node and should have gotten a PathNotFoundException ...
assertThat(separateThreadNode.get(), is(nullValue()));
assertThat(separateThreadException.get(), is(instanceOf(PathNotFoundException.class)));
// It should now be visible outside of the transaction ...
Session session3 = newSession();
Node grandChild3 = session3.getNode(path);
assertThat(grandChild.isSame(grandChild3), is(true));
assertEquals("bar", grandChild3.getProperty("foo").getString());
assertEquals("baz", grandChild3.getProperty("bar").getString());
session3.logout();
}
@Test
public void shouldBeAbleToVersionOutsideOfUserTransaction() throws Exception {
VersionManager vm = session.getWorkspace().getVersionManager();
Node node = session.getRootNode().addNode("Test3");
node.addMixin("mix:versionable");
node.setProperty("name", "lalalal");
node.setProperty("code", "lalalal");
session.save();
vm.checkin(node.getPath());
}
@Test
@FixFor( "MODE-2642" )
public void shouldBeAbleToVersionWithinUserTransactionAndDefaultTransactionManager() throws Exception {
startTransaction();
VersionManager vm = session.getWorkspace().getVersionManager();
Node node = session.getRootNode().addNode("Test3");
node.addMixin("mix:versionable");
node.setProperty("name", "lalalal");
node.setProperty("code", "lalalal");
session.save();
vm.checkin(node.getPath());
assertFalse(node.isCheckedOut());
try {
node.addMixin("mix:lockable");
fail("Expected a version exception because the node is checked in");
} catch (VersionException e) {
//expected
}
commitTransaction();
}
@FixFor( "MODE-1822" )
@Test
public void shouldBeAbleToVersionWithinUserTransaction() throws Exception {
// Start the repository using the JBoss Transactions transaction manager ...
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
// print = true;
startTransaction();
VersionManager vm = session.getWorkspace().getVersionManager();
printMessage("Looking for root node");
Node node = session.getRootNode().addNode("Test3");
node.addMixin("mix:versionable");
node.setProperty("name", "lalalal");
node.setProperty("code", "lalalal");
printMessage("Saving new node at " + node.getPath());
session.save();
vm.checkin(node.getPath());
printMessage("Checked in " + node.getPath());
for (int i = 0; i != 2; ++i) {
// Check it back out before we commit ...
node = session.getRootNode().getNode("Test3");
printMessage("Checking out " + node.getPath());
vm.checkout(node.getPath());
// Make some more changes ...
node.setProperty("code", "fa-lalalal");
printMessage("Saving changes to " + node.getPath());
session.save();
// Check it back in ...
printMessage("Checking in " + node.getPath());
vm.checkin(node.getPath());
}
commitTransaction();
}
@FixFor( "MODE-1822" )
@Test
public void shouldBeAbleToVersionWithinSequentialUserTransactions() throws Exception {
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
// print = true;
startTransaction();
VersionManager vm = session.getWorkspace().getVersionManager();
printMessage("Looking for root node");
Node node = session.getRootNode().addNode("Test3");
node.addMixin("mix:versionable");
node.setProperty("name", "lalalal");
node.setProperty("code", "lalalal");
printMessage("Saving new node at " + node.getPath());
session.save();
vm.checkin(node.getPath());
commitTransaction();
printMessage("Checked in " + node.getPath());
for (int i = 0; i != 2; ++i) {
// Check it back out before we commit ...
node = session.getRootNode().getNode("Test3");
printMessage("Checking out " + node.getPath());
vm.checkout(node.getPath());
// Make some more changes ...
startTransaction();
node.setProperty("code", "fa-lalalal");
printMessage("Saving changes to " + node.getPath());
session.save();
// Check it back in ...
printMessage("Checking in " + node.getPath());
vm.checkin(node.getPath());
commitTransaction();
}
}
@FixFor( "MODE-1822" )
@Test
public void shouldBeAbleToVersionWithinImmediatelySequentialUserTransactions() throws Exception {
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
// print = true;
startTransaction();
VersionManager vm = session.getWorkspace().getVersionManager();
printMessage("Looking for root node");
Node node = session.getRootNode().addNode("Test3");
node.addMixin("mix:versionable");
node.setProperty("name", "lalalal");
node.setProperty("code", "lalalal");
printMessage("Saving new node at " + node.getPath());
session.save();
vm.checkin(node.getPath());
commitTransaction();
printMessage("Checked in " + node.getPath());
for (int i = 0; i != 2; ++i) {
startTransaction();
// Check it back out before we change anything ...
printMessage("Checking out " + node.getPath());
vm.checkout(node.getPath());
printMessage("Checked out " + node.getPath());
// Make some more changes ...
node = session.getRootNode().getNode("Test3");
node.setProperty("code", "fa-lalalal");
printMessage("Saving changes to " + node.getPath());
session.save();
// Check it back in ...
printMessage("Checking in " + node.getPath());
vm.checkin(node.getPath());
commitTransaction();
}
}
@FixFor( "MODE-1822" )
@Test
public void shouldBeAbleToVersionWithinUserTransactionAndAtomikosTransactionManager() throws Exception {
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-atomikos.json");
startTransaction();
VersionManager vm = session.getWorkspace().getVersionManager();
Node node = session.getRootNode().addNode("Test3");
node.addMixin("mix:versionable");
node.setProperty("name", "lalalal");
node.setProperty("code", "lalalal");
session.save();
vm.checkin(node.getPath());
commitTransaction();
}
@Test
@FixFor( "MODE-2050" )
public void shouldBeAbleToUseNoClientTransactionsInMultithreadedEnvironment() throws Exception {
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
int threadsCount = 2;
ExecutorService executorService = Executors.newFixedThreadPool(threadsCount);
List<Future<Void>> results = new ArrayList<Future<Void>>(threadsCount);
final int nodesCount = 5;
for (int i = 0; i < threadsCount; i++) {
Future<Void> result = executorService.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
Session session = repository.login();
try {
String threadName = Thread.currentThread().getName();
for (int i = 0; i < nodesCount; i++) {
session.getRootNode().addNode("test_" + threadName);
session.save();
}
return null;
} finally {
session.logout();
}
}
});
results.add(result);
}
try {
for (Future<Void> result : results) {
result.get(10, TimeUnit.SECONDS);
result.cancel(true);
}
} finally {
executorService.shutdownNow();
}
}
@Test
@FixFor( "MODE-2352 " )
public void shouldSupportConcurrentWritersUpdatingTheSameNodeWithSeparateUserTransactions() throws Exception {
FileUtil.delete("target/persistent_repository");
startRepositoryWithConfigurationFrom("config/repo-config-new-workspaces.json");
int threadsCount = 10;
ExecutorService executorService = Executors.newFixedThreadPool(threadsCount);
List<Future<Void>> results = new ArrayList<>(threadsCount);
final AtomicInteger counter = new AtomicInteger(1);
for (int i = 0; i < threadsCount; i++) {
Future<Void> result = executorService.submit(() -> {
startTransaction();
Session session1 = repository.login();
session1.getRootNode().addNode("test_" + counter.incrementAndGet());
session1.save();
session1.logout();
commitTransaction();
return null;
});
results.add(result);
}
try {
for (Future<Void> result : results) {
result.get(10, TimeUnit.SECONDS);
}
Session session = repository.login();
// don't count jcr:system
assertEquals(threadsCount, session.getNode("/").getNodes().getSize() - 1);
session.logout();
} finally {
executorService.shutdownNow();
}
}
@FixFor( "MODE-2371" )
@Test
public void shouldInitializeWorkspacesWithOngoingUserTransaction() throws Exception {
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
startTransaction();
// the active tx should be suspended for the next call
Session otherSession = repository.login("otherWorkspace");
otherSession.logout();
commitTransaction();
startTransaction();
session.getWorkspace().createWorkspace("newWS");
session.logout();
commitTransaction();
otherSession = repository.login("newWS");
otherSession.logout();
startTransaction();
session = repository.login();
session.getWorkspace().createWorkspace("newWS1");
session.logout();
otherSession = repository.login("newWS1");
otherSession.logout();
commitTransaction();
}
@Test
@FixFor( "MODE-2395 " )
public void shouldSupportMultipleUpdatesFromTheSameSessionWithUserTransactions() throws Exception {
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
final JcrSession mainSession = repository.login();
startTransaction();
Node node1 = mainSession.getRootNode().addNode("node1");
node1.setProperty("prop", "foo");
mainSession.save();
commitTransaction();
startTransaction();
Node node2 = mainSession.getRootNode().addNode("node2");
node2.setProperty("prop", "foo");
mainSession.save();
commitTransaction();
// re-read the node to make sure it's in the cache
node2 = mainSession.getNode("/node2");
ExecutorService executorService = Executors.newFixedThreadPool(1);
try {
Future<Void> updaterResult = executorService.submit((Callable<Void>) () -> {
JcrSession updater = repository.login();
startTransaction();
AbstractJcrNode node21 = updater.getNode("/node2");
node21.setProperty("prop", "bar");
updater.save();
commitTransaction();
updater.logout();
return null;
});
updaterResult.get();
node2 = mainSession.getNode("/node2");
assertEquals("bar", node2.getProperty("prop").getString());
mainSession.logout();
} finally {
executorService.shutdownNow();
}
}
@Test
@FixFor( "MODE-2495" )
@Ignore( "ModeShape 5 requires thread confinement" )
public void shouldSupportMultipleThreadsChangingTheSameUserTransaction() throws Exception {
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
// STEP 1: create and checkin parent nodes
Node root = session.getRootNode();
Node parent = root.addNode("parent");
parent.addMixin("mix:versionable");
parent.addNode("nested");
session.save();
VersionManager vm = session.getWorkspace().getVersionManager();
vm.checkin("/parent");
// STEP 2: checkout, create child and checkin
vm = session.getWorkspace().getVersionManager();
vm.checkout("/parent");
Node nested = session.getNode("/parent/nested");
nested.addNode("child");
session.save();
vm.checkin("/parent");
// long transaction
final Transaction longTx = startTransaction();
// STEP 3: resume, checkout, suspend
vm = session.getWorkspace().getVersionManager();
vm.checkout("/parent");
session.removeItem("/parent/nested/child");
session.save();
suspendTransaction();
// STEP 4: check if child is still exists outside of longTx
Session s = repository.login();
s.getNode("/parent/nested/child");
s.logout();
// STEP 5: resume, checkin, commit
Thread t5 = new Thread(() -> {
try {
resumeTransaction(longTx);
VersionManager vm1 = session.getWorkspace().getVersionManager();
vm1.checkin("/parent");
commitTransaction();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
t5.start();
t5.join();
// STEP 6: check if child is gone
try {
Session s1 = repository.login();
s1.getNode("/parent/nested/child");
fail("should fail");
} catch (PathNotFoundException e) {
// expected
}
}
@Test
@FixFor( "MODE-2558" )
public void shouldNotCorruptDataWhenConcurrentlyWritingAndQuerying() throws Exception {
int threadCount = 150;
IntStream.range(0, threadCount).parallel().forEach(this::insertAndQueryNodes);
}
@Test
@FixFor( "MODE-2607" )
public void shouldUpdateParentAndRemoveChildWithDifferentTransactions1() throws Exception {
final String parentPath = "/parent";
final String childPath = "/parent/child";
//create parent and child node with some properties in a tx
startTransaction();
Session session = newSession();
Node parent = session.getRootNode().addNode("parent");
parent.setProperty("foo", "parent");
Node child = parent.addNode("child");
child.setProperty("foo", "child");
session.save();
commitTransaction();
//edit the parent and remove the child in a new tx
startTransaction();
session = newSession();
parent = session.getNode(parentPath);
parent.setProperty("foo", "bar2");
session.save();
child = session.getNode(childPath);
child.remove();
session.save();
commitTransaction();
//check that the editing worked in a new tx
startTransaction();
parent = session.getNode(parentPath);
assertEquals("bar2", parent.getProperty("foo").getString());
assertNoNode("/parent/child");
commitTransaction();
}
@Test
@FixFor( "MODE-2610" )
public void shouldUpdateParentAndRemoveChildWithDifferentTransactions2() throws Exception {
final String parentPath = "/parent";
final String childPath = "/parent/child";
startTransaction();
Node parent = session.getRootNode().addNode("parent");
parent.setProperty("foo", "parent");
Node child = parent.addNode("child");
child.setProperty("foo", "child");
session.save();
commitTransaction();
startTransaction();
child = session.getNode(childPath);
parent = session.getNode(parentPath);
parent.setProperty("foo", "bar2");
session.save();
child.remove();
session.save();
commitTransaction();
startTransaction();
parent = session.getNode(parentPath);
assertEquals("bar2", parent.getProperty("foo").getString());
assertNoNode("/parent/child");
session.logout();
commitTransaction();
}
@FixFor( "MODE-2623" )
@Test
public void shouldAllowLockUnlockWithinTransaction() throws Exception {
final String path = "/test";
Node parent = session.getRootNode().addNode("test");
parent.addMixin("mix:lockable");
session.save();
startTransaction();
LockManager lockMgr = session.getWorkspace().getLockManager();
lockMgr.lock(path, true, true, Long.MAX_VALUE, session.getUserID());
lockMgr.unlock(path);
commitTransaction();
assertFalse(session.getNode(path).isLocked());
}
@FixFor("MODE-2627")
@Test
public void shouldUpdateACLOnMovedNode() throws Exception {
AccessControlManager acm = session.getAccessControlManager();
final String childPath2 = "/parent/child2";
final String childDestinationNode = "/parent/child/child2";
JcrAccessControlList acl = new JcrAccessControlList(childDestinationNode);
Privileges privileges = new Privileges(session);
Privilege[] privilegeArray = new Privilege[] {
privileges.forName(Privilege.JCR_READ),
privileges.forName(Privilege.JCR_WRITE),
privileges.forName(Privilege.JCR_READ_ACCESS_CONTROL)
};
acl.addAccessControlEntry(SimplePrincipal.newInstance("anonymous"), privilegeArray);
startTransaction();
Node parent = session.getRootNode().addNode("parent");
parent.addNode("child");
parent.addNode("child2");
session.save();
commitTransaction();
startTransaction();
session.getWorkspace().move(childPath2, childDestinationNode);
session.save();
commitTransaction();
startTransaction();
acm.setPolicy(childDestinationNode, acl);
session.save();
Node movedNode = session.getNode(childDestinationNode);
assertEquals(childDestinationNode, movedNode.getPath());
assertEquals(1, acm.getPolicies(childDestinationNode).length);
assertEquals(acm.getPolicies(childDestinationNode)[0], acl);
assertNoNode(childPath2);
session.logout();
commitTransaction();
}
@Test
@FixFor( "MODE-2642" )
public void shouldLockNodeWithinTransaction() throws Exception {
Node node = session.getRootNode().addNode("test");
node.addMixin("mix:lockable");
session.save();
startTransaction();
JcrLockManager lockManager = session.getWorkspace().getLockManager();
Lock lock = lockManager.lock(node.getPath(), false, false, Long.MAX_VALUE, null);
assertTrue(lock.isLive());
assertTrue("Node should be locked", node.isLocked());
commitTransaction();
assertTrue(node.isLocked());
}
@Test
@FixFor( "MODE-2668" )
public void shouldRaiseExceptionAndRollbackIfTransactionFails1() throws Exception {
session.getRootNode().addNode("parent");
session.save();
Transaction tx = startTransaction();
tx.rollback();
AbstractJcrNode parent = session.getNode("/parent");
parent.addNode("child");
try {
session.save();
fail("Expected save operation to fail");
} catch (RepositoryException e) {
assertTrue(e.getCause() instanceof IllegalStateException);
session.refresh(false);
}
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
CompletableFuture.runAsync(() -> {
try {
Transaction tx1 = startTransaction();
session.getNode("/parent").addNode("child");
session.save();
tx1.commit();
assertEquals(1, parent.getNodes().getSize());
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executorService).get();
} finally {
transactionManager().suspend();
executorService.shutdownNow();
}
}
@Test
@FixFor( "MODE-2668" )
public void shouldRaiseExceptionAndRollbackIfTransactionFails2() throws Exception {
Node parent = session.getRootNode().addNode("parent");
session.save();
Transaction tx = startTransaction();
// add one child
parent.addNode("child1");
session.save();
// explicitly rollback
tx.rollback();
try {
parent.addNode("child2");
session.save();
fail("Expected save operation to fail");
} catch (RepositoryException e) {
assertTrue(e.getCause() instanceof IllegalStateException);
session.refresh(false);
}
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
CompletableFuture.runAsync(() -> {
try {
Transaction tx1 = startTransaction();
parent.addNode("child1");
parent.addNode("child2");
session.save();
tx1.commit();
assertEquals(2, parent.getNodes().getSize());
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executorService).get();
} finally {
transactionManager().suspend();
executorService.shutdownNow();
}
}
@Test
@FixFor("MODE-2670")
public void shouldRollbackAndCommitTransactionsFromDifferentThreadsForFileProvider() throws Exception {
// Start the repository using the JBoss Transactions transaction manager ...
startRepositoryWithConfigurationFrom("config/repo-config-inmemory-txn.json");
testCommitAndRollbackOffSeparateThreads();
}
@Test
@FixFor("MODE-2670")
public void shouldRollbackAndCommitTransactionsFromDifferentThreadsForDBProvider() throws Exception {
FileUtil.delete("target/txn");
// Start the repository using the JBoss Transactions transaction manager ...
startRepositoryWithConfigurationFrom("config/repo-config-db-txn.json");
testCommitAndRollbackOffSeparateThreads();
}
private void testCommitAndRollbackOffSeparateThreads() throws Exception {
session.getRootNode().addNode("parent");
session.save();
ExecutorService executorService = Executors.newFixedThreadPool(2);
try {
transactionManager().begin();
Transaction tx = transactionManager().getTransaction();
assertNotNull(tx);
session.getNode("/parent").addNode("child");
session.save();
executorService.submit(() -> {
tx.rollback();
return null;
}).get(2, TimeUnit.SECONDS);
//nothing should be there
assertNoNode("/parent/child");
// and the tx off thread 'main' should be aborted
Transaction afterRollback = transactionManager().getTransaction();
assertNotNull(afterRollback);
assertEquals(Status.STATUS_ROLLEDBACK, afterRollback.getStatus());
// remove it from the current thread
transactionManager().suspend();
//now make the same change and commit from a separate thread
executorService.submit(() -> {
transactionManager().begin();
final Transaction tx1 = transactionManager().getTransaction();
assertNotNull(tx1);
assertEquals(Status.STATUS_ACTIVE, tx1.getStatus());
session.getNode("/parent").addNode("child");
session.save();
executorService.submit(() -> {
tx1.commit();
return null;
}).get(2, TimeUnit.SECONDS);
// we've committed off a different thread, but the changes should be persisted
try {
assertNode("/parent/child");
return null;
} finally {
//remove it from the current thread
transactionManager().suspend();
}
}).get(2, TimeUnit.SECONDS);
assertNode("/parent/child");
assertNull(transactionManager().getTransaction());
} catch (java.util.concurrent.TimeoutException te) {
fail("Timeout detected; this means threads are not able to complete due to a lock starvation");
}
finally {
executorService.shutdownNow();
}
}
private void insertAndQueryNodes(int i) {
Session session = null;
try {
startTransaction();
session = repository.login();
createNode("/", UUID.randomUUID().toString(), session);
commitTransaction();
session = repository.login();
QueryManager queryManager = session.getWorkspace().getQueryManager();
Query query = queryManager.createQuery("SELECT node.* FROM [mix:title] AS node", Query.JCR_SQL2);
QueryResult result = query.execute();
assertTrue(result.getNodes().getSize() > 0);
session.logout();
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException)e;
} else {
throw new RuntimeException(e);
}
} finally {
if (session != null) {
session.logout();
session = null;
}
}
}
private Node createNode(String parentNodePath, String uuid, Session session) throws RepositoryException {
String nodeLevelOneName = uuid.substring(0, 2);
String nodeLevelOnePath = parentNodePath + "/" + nodeLevelOneName;
String nodeLevelTwoName = uuid.substring(2, 4);
String nodeLevelTwoPath = nodeLevelOnePath + "/" + nodeLevelTwoName;
String nodeLevelThreeName = uuid.substring(4, 6);
String nodeLevelThreePath = nodeLevelTwoPath + "/" + nodeLevelThreeName;
addLevel(parentNodePath, nodeLevelOneName, nodeLevelOnePath, session);
addLevel(nodeLevelOnePath, nodeLevelTwoName, nodeLevelTwoPath, session);
addLevel(nodeLevelTwoPath, nodeLevelThreeName, nodeLevelThreePath, session);
session.save();
Node parent = session.getNode(parentNodePath);
Node node = parent.addNode(uuid);
node.addMixin("mix:title");
node.setProperty("jcr:title", "test");
session.save();
return node;
}
private void addLevel(String currentNodeLevelPath, String nextNodeLevelName, String nextNodeLevelPath, Session session) throws RepositoryException {
if (!session.nodeExists(nextNodeLevelPath)) {
Node parentNode = session.getNode(currentNodeLevelPath);
parentNode.addNode(nextNodeLevelName);
}
}
protected Transaction suspendTransaction() throws SystemException {
TransactionManager txnMgr = transactionManager();
return txnMgr.suspend();
}
protected void resumeTransaction(Transaction t) throws InvalidTransactionException, IllegalStateException, SystemException {
TransactionManager txnMgr = transactionManager();
txnMgr.resume(t);
}
protected Transaction startTransaction() throws NotSupportedException, SystemException {
TransactionManager txnMgr = transactionManager();
// Change this to true if/when debugging ...
try {
txnMgr.setTransactionTimeout(1000);
} catch (Exception e) {
// ignore
}
txnMgr.begin();
return txnMgr.getTransaction();
}
protected void commitTransaction()
throws SystemException, SecurityException, IllegalStateException, RollbackException, HeuristicMixedException,
HeuristicRollbackException {
TransactionManager txnMgr = transactionManager();
txnMgr.commit();
}
protected TransactionManager transactionManager() {
return session.getRepository().transactionManager();
}
private void moveDocument(String nodeName) throws Exception {
Node section = session.getRootNode().addNode(nodeName);
section.setProperty("name", nodeName);
section.addNode("temppath");
session.save();
String srcAbsPath = "/" + nodeName + "/temppath";
String destAbsPath = "/" + nodeName + "/20130104";
session.move(srcAbsPath, destAbsPath);
session.save();
NodeIterator nitr = section.getNodes();
if (print) {
System.err.println("Child Nodes of " + nodeName + " are:");
while (nitr.hasNext()) {
Node n = nitr.nextNode();
System.err.println(" Node: " + n.getName());
}
}
}
}