/*
* (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Thierry Delprat
* Florent Guillaume
*/
package org.nuxeo.ecm.core.event.impl;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.logging.SequenceTracer;
import org.nuxeo.ecm.core.event.EventBundle;
import org.nuxeo.ecm.core.event.EventStats;
import org.nuxeo.ecm.core.event.ReconnectedEventBundle;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.transaction.TransactionHelper;
/**
* Executor that passes an event bundle to post-commit asynchronous listeners (in a separated thread in order to manage
* transactions).
* <p>
* Allows a bulk mode where transaction management is not per-listener done once for the whole set of listeners.
*/
public class PostCommitEventExecutor {
private static final Log log = LogFactory.getLog(PostCommitEventExecutor.class);
public static final String TIMEOUT_MS_PROP = "org.nuxeo.ecm.core.event.tx.PostCommitExecutor.timeoutMs";
public static final int DEFAULT_TIMEOUT_MS = 300; // 0.3s
public static final int DEFAULT_TIMEOUT_TEST_MS = 60000; // 1 min
private Integer defaultTimeoutMs;
public static final String DEFAULT_BULK_TIMEOUT_S = "600"; // 10min
public static final String BULK_TIMEOUT_PROP = "org.nuxeo.ecm.core.event.tx.BulkExecutor.timeout";
private static final long KEEP_ALIVE_TIME_SECOND = 10;
private static final int MAX_POOL_SIZE = 100;
protected final ExecutorService executor;
/**
* Creates non-daemon threads at normal priority.
*/
private static class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger();
private final ThreadGroup group;
private final String prefix;
public NamedThreadFactory(String prefix) {
SecurityManager sm = System.getSecurityManager();
group = sm == null ? Thread.currentThread().getThreadGroup() : sm.getThreadGroup();
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
String name = prefix + threadNumber.incrementAndGet();
Thread thread = new Thread(group, r, name);
// do not set daemon
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}
public PostCommitEventExecutor() {
// use as much thread as needed up to MAX_POOL_SIZE
// keep them alive a moment for reuse
// have all threads torn down when there is no work to do
ThreadFactory threadFactory = new NamedThreadFactory("Nuxeo-Event-PostCommit-");
executor = new ThreadPoolExecutor(0, MAX_POOL_SIZE, KEEP_ALIVE_TIME_SECOND, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), threadFactory);
((ThreadPoolExecutor) executor).allowCoreThreadTimeOut(true);
}
protected int getDefaultTimeoutMs() {
if (defaultTimeoutMs == null) {
if (Framework.getProperty(TIMEOUT_MS_PROP) != null) {
defaultTimeoutMs = Integer.parseInt(Framework.getProperty(TIMEOUT_MS_PROP));
} else if (Framework.isTestModeSet()) {
defaultTimeoutMs = DEFAULT_TIMEOUT_TEST_MS;
} else {
defaultTimeoutMs = DEFAULT_TIMEOUT_MS;
}
}
return defaultTimeoutMs;
}
public void shutdown(long timeoutMillis) throws InterruptedException {
executor.shutdown();
executor.awaitTermination(timeoutMillis, TimeUnit.MILLISECONDS);
if (!executor.isTerminated()) {
executor.shutdownNow();
}
}
public void run(List<EventListenerDescriptor> listeners, EventBundle event) {
run(listeners, event, getDefaultTimeoutMs(), false);
}
public void runBulk(List<EventListenerDescriptor> listeners, EventBundle event) {
String timeoutSeconds = Framework.getProperty(BULK_TIMEOUT_PROP, DEFAULT_BULK_TIMEOUT_S);
run(listeners, event, Long.parseLong(timeoutSeconds) * 1000, true);
}
public void run(List<EventListenerDescriptor> listeners, EventBundle bundle, long timeoutMillis, boolean bulk) {
// check that there's at list one listener interested
boolean some = false;
for (EventListenerDescriptor listener : listeners) {
if (listener.acceptBundle(bundle)) {
some = true;
break;
}
}
if (!some) {
if (log.isDebugEnabled()) {
log.debug("Events postcommit execution has nothing to do");
}
return;
}
if (log.isDebugEnabled()) {
log.debug(String.format("Events postcommit execution starting with timeout %sms%s",
Long.valueOf(timeoutMillis), bulk ? " in bulk mode" : ""));
}
Callable<Boolean> callable = !bulk ? new EventBundleRunner(listeners, bundle) : new EventBundleBulkRunner(
listeners, bundle);
FutureTask<Boolean> futureTask = new FutureTask<Boolean>(callable);
try {
executor.execute(futureTask);
} catch (RejectedExecutionException e) {
log.error("Events postcommit execution rejected", e);
return;
}
try {
// wait for runner to be finished, with timeout
Boolean ok = futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
if (Boolean.FALSE.equals(ok)) {
log.error("Events postcommit bulk execution aborted due to previous error");
}
} catch (InterruptedException e) {
// restore interrupted status
Thread.currentThread().interrupt();
// interrupt thread
futureTask.cancel(true); // mayInterruptIfRunning=true
} catch (TimeoutException e) {
if (!bulk) {
log.info(String.format("Events postcommit execution exceeded timeout of %sms, leaving thread running",
Long.valueOf(timeoutMillis)));
// don't cancel task, let it run
} else {
log.error(String.format(
"Events postcommit bulk execution exceeded timeout of %sms, interrupting thread",
Long.valueOf(timeoutMillis)));
futureTask.cancel(true); // mayInterruptIfRunning=true
}
} catch (ExecutionException e) {
log.error("Events postcommit execution encountered unexpected exception", e.getCause());
}
if (log.isDebugEnabled()) {
log.debug("Events postcommit execution finished");
}
}
/**
* Lets the listeners process the event bundle.
* <p>
* For each listener, the event bundle is reconnected to a session and a transaction is started.
* <p>
* In case of exception in a listener, the transaction is rolled back for that listener but processing continues for
* the other listeners.
* <p>
* In case of timeout, an error is logged but processing continues for the other listeners (the thread is left
* running separately from the main thread that initiated post-commit processing).
*/
protected static class EventBundleRunner implements Callable<Boolean> {
protected final List<EventListenerDescriptor> listeners;
protected final EventBundle bundle;
protected String callerThread;
public EventBundleRunner(List<EventListenerDescriptor> listeners, EventBundle bundle) {
this.listeners = listeners;
this.bundle = bundle;
callerThread = SequenceTracer.getThreadName();
}
@Override
public Boolean call() {
if (log.isDebugEnabled()) {
log.debug("Events postcommit execution starting in thread: " + Thread.currentThread().getName());
}
SequenceTracer.startFrom(callerThread, "Postcommit", "#ff410f");
long t0 = System.currentTimeMillis();
EventStats stats = Framework.getLocalService(EventStats.class);
for (EventListenerDescriptor listener : listeners) {
EventBundle filtered = listener.filterBundle(bundle);
if (filtered.isEmpty()) {
continue;
}
if (log.isDebugEnabled()) {
log.debug("Events postcommit execution start for listener: " + listener.getName());
}
SequenceTracer.start("run listener " + listener.getName());
long t1 = System.currentTimeMillis();
boolean ok = false;
ReconnectedEventBundle reconnected = null;
// transaction timeout is managed by the FutureTask
boolean tx = TransactionHelper.startTransaction();
try {
reconnected = new ReconnectedEventBundleImpl(filtered, listeners.toString());
listener.asPostCommitListener().handleEvent(reconnected);
if (Thread.currentThread().isInterrupted()) {
log.error("Events postcommit execution interrupted for listener: " + listener.getName());
SequenceTracer.destroy("interrupted");
ok = false;
} else {
ok = true;
}
} catch (RuntimeException e) {
log.error("Events postcommit execution encountered exception for listener: " + listener.getName(),
e);
// don't rethrow, but rollback (ok=false) and continue loop
} finally {
try {
if (reconnected != null) {
reconnected.disconnect();
}
} finally {
if (tx) {
if (!ok) {
TransactionHelper.setTransactionRollbackOnly();
log.error("Rolling back transaction");
}
TransactionHelper.commitOrRollbackTransaction();
}
long elapsed = System.currentTimeMillis() - t1;
if (stats != null) {
stats.logAsyncExec(listener, elapsed);
}
if (log.isDebugEnabled()) {
log.debug("Events postcommit execution end for listener: " + listener.getName() + " in "
+ elapsed + "ms");
}
SequenceTracer.stop("listener done " + elapsed + " ms");
}
}
// even if interrupted due to timeout, we continue the loop
}
long elapsed = System.currentTimeMillis() - t0;
if (log.isDebugEnabled()) {
log.debug("Events postcommit execution finished in " + elapsed + "ms");
}
SequenceTracer.stop("postcommit done" + elapsed + " ms");
return Boolean.TRUE; // no error to report
}
}
/**
* Lets the listeners process the event bundle in bulk mode.
* <p>
* The event bundle is reconnected to a single session and a single transaction is started for all the listeners.
* <p>
* In case of exception in a listener, the transaction is rolled back and processing stops.
* <p>
* In case of timeout, the transaction is rolled back and processing stops.
*/
protected static class EventBundleBulkRunner implements Callable<Boolean> {
protected final List<EventListenerDescriptor> listeners;
protected final EventBundle bundle;
protected final String callerThread;
public EventBundleBulkRunner(List<EventListenerDescriptor> listeners, EventBundle bundle) {
this.listeners = listeners;
this.bundle = bundle;
callerThread = SequenceTracer.getThreadName();
}
@Override
public Boolean call() {
SequenceTracer.startFrom(callerThread, "BulkPostcommit", "#ff410f");
if (log.isDebugEnabled()) {
log.debug("Events postcommit bulk execution starting in thread: " + Thread.currentThread().getName());
}
long t0 = System.currentTimeMillis();
boolean ok = false;
boolean interrupt = false;
ReconnectedEventBundle reconnected = null;
// transaction timeout is managed by the FutureTask
boolean tx = TransactionHelper.startTransaction();
try {
reconnected = new ReconnectedEventBundleImpl(bundle, listeners.toString());
for (EventListenerDescriptor listener : listeners) {
EventBundle filtered = listener.filterBundle(reconnected);
if (filtered.isEmpty()) {
continue;
}
SequenceTracer.start("run listener " + listener.getName());
if (log.isDebugEnabled()) {
log.debug("Events postcommit bulk execution start for listener: " + listener.getName());
}
long t1 = System.currentTimeMillis();
try {
listener.asPostCommitListener().handleEvent(filtered);
if (Thread.currentThread().isInterrupted()) {
log.error("Events postcommit bulk execution interrupted for listener: "
+ listener.getName() + ", will rollback and abort bulk processing");
interrupt = true;
}
} catch (RuntimeException e) {
log.error(
"Events postcommit bulk execution encountered exception for listener: "
+ listener.getName(), e);
return Boolean.FALSE; // report error
} finally {
long elapsed = System.currentTimeMillis() - t1;
if (log.isDebugEnabled()) {
log.debug("Events postcommit bulk execution end for listener: " + listener.getName()
+ " in " + elapsed + "ms");
}
SequenceTracer.stop("listener done " + elapsed + " ms");
}
if (interrupt) {
break;
}
}
ok = !interrupt;
} finally {
try {
if (reconnected != null) {
reconnected.disconnect();
}
} finally {
if (tx) {
if (!ok) {
TransactionHelper.setTransactionRollbackOnly();
log.error("Rolling back transaction");
}
TransactionHelper.commitOrRollbackTransaction();
}
}
long elapsed = System.currentTimeMillis() - t0;
SequenceTracer.stop("BulkPostcommit done " + elapsed + " ms");
if (log.isDebugEnabled()) {
log.debug("Events postcommit bulk execution finished in " + elapsed + "ms");
}
}
return Boolean.TRUE; // no error to report
}
}
}