/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.jackrabbit.core; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; 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 javax.jcr.Node; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.lock.LockException; import org.apache.jackrabbit.test.AbstractJCRTest; import org.apache.jackrabbit.util.LockedWrapper; /** * tests for the utility class {@link org.apache.jackrabbit.util.LockedWrapper}. */ public class LockedWrapperTest extends AbstractJCRTest { private static final int NUM_THREADS = 100; private static final int NUM_CHANGES = 10; private static final int NUM_VALUE_GETS = 10; private final Random random = new Random(); private AtomicInteger counter = new AtomicInteger(0); /** * Tests the utility {@link org.apache.jackrabbit.util.Locked} by * implementing running multiple threads concurrently that apply changes to * a lockable node. */ public void testConcurrentUpdates() throws RepositoryException, InterruptedException { final Node lockable = testRootNode.addNode(nodeName1); lockable.addMixin(mixLockable); superuser.save(); List<Future<?>> futures = new ArrayList<Future<?>>(); ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); for (int i = 0; i < NUM_THREADS; i++) { futures.add(e.submit(buildNewConcurrentUpdateCallable(i, lockable.getPath(), false))); } e.shutdown(); assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); for (Future<?> future : futures) { try { future.get(); } catch (ExecutionException ex) { fail(ex.getMessage()); } } } /** * Tests the utility {@link org.apache.jackrabbit.util.Locked} by * implementing running multiple threads concurrently that apply changes to * a lockable node, also refreshing the session on each operation */ public void testConcurrentUpdatesWithSessionRefresh() throws RepositoryException, InterruptedException { final Node lockable = testRootNode.addNode(nodeName1); lockable.addMixin(mixLockable); superuser.save(); List<Future<?>> futures = new ArrayList<Future<?>>(); ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); for (int i = 0; i < NUM_THREADS; i++) { futures.add(e.submit(buildNewConcurrentUpdateCallable(i, lockable.getPath(), true))); } e.shutdown(); assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); for (Future<?> future : futures) { try { future.get(); } catch (ExecutionException ex) { fail(ex.getMessage()); } } } public Callable<Void> buildNewConcurrentUpdateCallable( final int threadNumber, final String lockablePath, final boolean withSessionRefresh) { return new Callable<Void>() { public Void call() throws RepositoryException, InterruptedException { final Session s = getHelper().getSuperuserSession(); try { for (int i = 0; i < NUM_CHANGES; i++) { if (withSessionRefresh) { s.refresh(false); } Node n = s.getNode(lockablePath); new LockedWrapper<Void>() { @Override protected Void run(Node node) throws RepositoryException { String nodeName = "node" + threadNumber; if (node.hasNode(nodeName)) { node.getNode(nodeName).remove(); } else { node.addNode(nodeName); } s.save(); log.println("Thread" + threadNumber + ": saved modification"); return null; } }.with(n, false); // do a random wait Thread.sleep(random.nextInt(100)); } } finally { s.logout(); } return null; } }; } public void testTimeout() throws RepositoryException, InterruptedException { final Node lockable = testRootNode.addNode("testTimeout" + System.currentTimeMillis()); lockable.addMixin(mixLockable); superuser.save(); ExecutorService e = Executors.newFixedThreadPool(2); Future<?> lockMaster = e.submit(buildNewTimeoutCallable( lockable.getPath(), true)); Future<?> lockSlave = e.submit(buildNewTimeoutCallable( lockable.getPath(), false)); e.shutdown(); try { lockMaster.get(); lockSlave.get(); } catch (ExecutionException ex) { if (ex.getCause().getClass().isAssignableFrom(LockException.class)) { return; } fail(ex.getMessage()); } fail("was expecting a LockException"); } public Callable<Void> buildNewTimeoutCallable(final String lockablePath, final boolean isLockMaster) { return new Callable<Void>() { public Void call() throws RepositoryException, InterruptedException { // allow master to be first to obtain the lock on the node if (!isLockMaster) { TimeUnit.SECONDS.sleep(2); } final Session s = getHelper().getSuperuserSession(); try { Node n = s.getNode(lockablePath); new LockedWrapper<Void>() { @Override protected Void run(Node node) throws RepositoryException { if (isLockMaster) { try { TimeUnit.SECONDS.sleep(15); } catch (InterruptedException e) { // } } return null; } }.with(n, false, 2000); } finally { s.logout(); } return null; } }; } /** * Tests concurrent updates on a persistent counter */ public void testSequence() throws RepositoryException, InterruptedException { counter = new AtomicInteger(0); final Node lockable = testRootNode.addNode(nodeName1); lockable.setProperty("value", counter.getAndIncrement()); lockable.addMixin(mixLockable); superuser.save(); List<Future<Long>> futures = new ArrayList<Future<Long>>(); ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); for (int i = 0; i < NUM_THREADS * NUM_VALUE_GETS; i++) { futures.add(e.submit(buildNewSequenceUpdateCallable( lockable.getPath(), false))); } e.shutdown(); assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); for (Future<Long> future : futures) { try { Long v = future.get(); log.println("Got sequence number: " + v); } catch (ExecutionException ex) { fail(ex.getMessage()); } } } /** * Tests concurrent updates on a persistent counter, with session refresh */ public void testSequenceWithSessionRefresh() throws RepositoryException, InterruptedException { counter = new AtomicInteger(0); final Node lockable = testRootNode.addNode(nodeName1); lockable.setProperty("value", counter.getAndIncrement()); lockable.addMixin(mixLockable); superuser.save(); List<Future<Long>> futures = new ArrayList<Future<Long>>(); ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); for (int i = 0; i < NUM_THREADS * NUM_VALUE_GETS; i++) { futures.add(e.submit(buildNewSequenceUpdateCallable( lockable.getPath(), true))); } e.shutdown(); assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); for (Future<Long> future : futures) { try { Long v = future.get(); log.println("Got sequence number: " + v); } catch (ExecutionException ex) { fail(ex.getMessage()); } } } public Callable<Long> buildNewSequenceUpdateCallable( final String counterPath, final boolean withSessionRefresh) { return new Callable<Long>() { public Long call() throws RepositoryException, InterruptedException { final Session s = getHelper().getSuperuserSession(); try { if (withSessionRefresh) { s.refresh(false); } Node n = s.getNode(counterPath); long value = new LockedWrapper<Long>() { @Override protected Long run(Node node) throws RepositoryException { Property seqProp = node.getProperty("value"); long value = seqProp.getLong(); seqProp.setValue(++value); s.save(); return value; } }.with(n, false); // check that the sequence is ok assertEquals(counter.getAndIncrement(), value); // do a random wait Thread.sleep(random.nextInt(100)); return value; } finally { s.logout(); } } }; } }