/*
* 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 org.apache.jackrabbit.test.AbstractJCRTest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Session;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
/**
* <code>AbstractConcurrencyTest</code> provides utility methods to run tests
* using concurrent threads.
*/
public abstract class AbstractConcurrencyTest extends AbstractJCRTest {
/**
* Logger instance for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AbstractConcurrencyTest.class);
/**
* Runs a task with the given concurrency and creates an individual test
* node for each thread.
*
* @param task the task to run.
* @param concurrency the concurrency.
* @throws RepositoryException if an error occurs.
*/
protected void runTask(Task task, int concurrency) throws RepositoryException {
runTasks(new Task[]{task}, concurrency,
// run for at most one year ;)
getOneYearAhead());
}
/**
* Runs each of the tasks with the given concurrency and creates an
* individual test node for each thread.
*
* @param tasks the tasks to run.
* @param concurrency the concurrency.
* @param timeout when System.currentTimeMillis() reaches timeout the
* threads executing the tasks should be interrupted.
* This indicates that a deadlock occured.
* @throws RepositoryException if an error occurs.
*/
protected void runTasks(Task[] tasks, int concurrency, long timeout)
throws RepositoryException {
Executor[] executors = new Executor[concurrency * tasks.length];
for (int t = 0; t < tasks.length; t++) {
for (int i = 0; i < concurrency; i++) {
int id = t * concurrency + i;
Session s = getHelper().getSuperuserSession();
Node test = s.getRootNode().addNode(testPath + "/node" + id);
s.save();
executors[id] = new Executor(s, test, tasks[t]);
}
}
executeAll(executors, timeout);
}
/**
* Runs a task with the given concurrency on the node identified by path.
*
* @param task the task to run.
* @param concurrency the concurrency.
* @param path the path to the test node.
* @throws RepositoryException if an error occurs.
*/
protected void runTask(Task task, int concurrency, String path)
throws RepositoryException {
Executor[] executors = new Executor[concurrency];
for (int i = 0; i < concurrency; i++) {
Session s = getHelper().getSuperuserSession();
Node test = (Node) s.getItem(path);
s.save();
executors[i] = new Executor(s, test, task);
}
executeAll(executors, getOneYearAhead());
}
/**
* Executes all executors using individual threads.
*
* @param executors the executors.
* @param timeout time when running threads should be interrupted.
* @throws RepositoryException if one of the executors throws an exception.
*/
private void executeAll(Executor[] executors, long timeout) throws RepositoryException {
Thread[] threads = new Thread[executors.length];
for (int i = 0; i < executors.length; i++) {
threads[i] = new Thread(executors[i], "Executor " + i);
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
boolean stacksDumped = false;
for (int i = 0; i < threads.length; i++) {
try {
long wait = Math.max(timeout - System.currentTimeMillis(), 1000);
threads[i].join(wait);
if (threads[i].isAlive()) {
if (!stacksDumped) {
dumpStacks(threads);
stacksDumped = true;
}
threads[i].interrupt();
// give the thread a couple of seconds, then call stop
Thread.sleep(5 * 1000);
if (threads[i].isAlive()) {
threads[i].stop();
}
}
} catch (InterruptedException e) {
// ignore
}
}
for (int i = 0; i < executors.length; i++) {
if (executors[i].getException() != null) {
throw executors[i].getException();
}
}
}
protected long getOneYearAhead() {
return System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 30L * 12L;
}
/**
* If tests are run in a 1.5 JVM or higher the stack of the given threads
* are dumped to the logger with level ERROR.
*
* @param threads An array of threads.
*/
protected static void dumpStacks(Thread[] threads) {
try {
Method m = Thread.class.getMethod("getStackTrace", null);
StringBuffer dumps = new StringBuffer();
for (int t = 0; t < threads.length; t++) {
StackTraceElement[] elements = (StackTraceElement[]) m.invoke(
threads[t], null);
dumps.append(threads[t].toString()).append('\n');
for (int i = 0; i < elements.length; i++) {
dumps.append("\tat " + elements[i]).append('\n');
}
dumps.append('\n');
}
logger.error("Thread dumps:\n{}", dumps);
} catch (NoSuchMethodException e) {
// not a 1.5 JVM
} catch (IllegalAccessException e) {
// ignore
} catch (InvocationTargetException e) {
// ignore
}
}
/**
* Task implementations must be thread safe! Multiple threads will call
* {@link #execute(Session, Node)} concurrently.
*/
public interface Task {
public abstract void execute(Session session, Node test)
throws RepositoryException;
}
protected static class Executor implements Runnable {
protected final Session session;
protected final Node test;
protected final Task task;
protected RepositoryException exception;
public Executor(Session session, Node test, Task task) {
this.session = session;
this.test = test;
this.task = task;
}
public RepositoryException getException() {
return exception;
}
public void run() {
try {
task.execute(session, test);
} catch (RepositoryException e) {
exception = e;
} finally {
session.logout();
}
}
}
}