/*
* (C) Copyright 2006-2016 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:
* Bogdan Stefanescu
* Thierry Delprat
* Florent Guillaume
* Andrei Nechaev
*/
package org.nuxeo.ecm.core.event.impl;
import java.rmi.dgc.VMID;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import javax.naming.NamingException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.logging.SequenceTracer;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.RecoverableClientException;
import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventBundle;
import org.nuxeo.ecm.core.event.EventContext;
import org.nuxeo.ecm.core.event.EventListener;
import org.nuxeo.ecm.core.event.EventService;
import org.nuxeo.ecm.core.event.EventServiceAdmin;
import org.nuxeo.ecm.core.event.EventStats;
import org.nuxeo.ecm.core.event.PostCommitEventListener;
import org.nuxeo.ecm.core.event.ReconnectedEventBundle;
import org.nuxeo.ecm.core.event.jms.AsyncProcessorConfig;
import org.nuxeo.ecm.core.event.pipe.EventPipeDescriptor;
import org.nuxeo.ecm.core.event.pipe.EventPipeRegistry;
import org.nuxeo.ecm.core.event.pipe.dispatch.EventBundleDispatcher;
import org.nuxeo.ecm.core.event.pipe.dispatch.EventDispatcherDescriptor;
import org.nuxeo.ecm.core.event.pipe.dispatch.EventDispatcherRegistry;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.transaction.TransactionHelper;
/**
* Implementation of the event service.
*/
public class EventServiceImpl implements EventService, EventServiceAdmin, Synchronization {
public static final VMID VMID = new VMID();
private static final Log log = LogFactory.getLog(EventServiceImpl.class);
protected static final ThreadLocal<CompositeEventBundle> threadBundles = new ThreadLocal<CompositeEventBundle>() {
@Override
protected CompositeEventBundle initialValue() {
return new CompositeEventBundle();
}
};
private static class CompositeEventBundle {
boolean registeredSynchronization;
final Map<String, EventBundle> byRepository = new HashMap<>();
void push(Event event) {
String repositoryName = event.getContext().getRepositoryName();
if (!byRepository.containsKey(repositoryName)) {
byRepository.put(repositoryName, new EventBundleImpl());
}
byRepository.get(repositoryName).push(event);
}
}
protected final EventListenerList listenerDescriptors;
protected PostCommitEventExecutor postCommitExec;
protected volatile AsyncEventExecutor asyncExec;
protected final List<AsyncWaitHook> asyncWaitHooks = new CopyOnWriteArrayList<>();
protected boolean blockAsyncProcessing = false;
protected boolean blockSyncPostCommitProcessing = false;
protected boolean bulkModeEnabled = false;
protected EventPipeRegistry registeredPipes = new EventPipeRegistry();
protected EventDispatcherRegistry dispatchers = new EventDispatcherRegistry();
protected EventBundleDispatcher pipeDispatcher;
public EventServiceImpl() {
listenerDescriptors = new EventListenerList();
postCommitExec = new PostCommitEventExecutor();
asyncExec = new AsyncEventExecutor();
}
public void init() {
asyncExec.init();
EventDispatcherDescriptor dispatcherDescriptor = dispatchers.getDispatcherDescriptor();
if (dispatcherDescriptor != null) {
List<EventPipeDescriptor> pipes = registeredPipes.getPipes();
if (!pipes.isEmpty()) {
pipeDispatcher = dispatcherDescriptor.getInstance();
pipeDispatcher.init(pipes, dispatcherDescriptor.getParameters());
}
}
}
public EventBundleDispatcher getEventBundleDispatcher() {
return pipeDispatcher;
}
public void shutdown(long timeoutMillis) throws InterruptedException {
postCommitExec.shutdown(timeoutMillis);
Set<AsyncWaitHook> notTerminated = asyncWaitHooks.stream().filter(hook -> !hook.shutdown()).collect(
Collectors.toSet());
if (!notTerminated.isEmpty()) {
throw new RuntimeException("Asynch services are still running : " + notTerminated);
}
if (!asyncExec.shutdown(timeoutMillis)) {
throw new RuntimeException("Async executor is still running, timeout expired");
}
if (pipeDispatcher != null) {
pipeDispatcher.shutdown();
}
}
public void registerForAsyncWait(AsyncWaitHook callback) {
asyncWaitHooks.add(callback);
}
public void unregisterForAsyncWait(AsyncWaitHook callback) {
asyncWaitHooks.remove(callback);
}
@Override
public void waitForAsyncCompletion() {
waitForAsyncCompletion(Long.MAX_VALUE);
}
@Override
public void waitForAsyncCompletion(long timeout) {
Set<AsyncWaitHook> notCompleted = asyncWaitHooks.stream()
.filter(hook -> !hook.waitForAsyncCompletion())
.collect(Collectors.toSet());
if (!notCompleted.isEmpty()) {
throw new RuntimeException("Async tasks are still running : " + notCompleted);
}
try {
if (!asyncExec.waitForCompletion(timeout)) {
throw new RuntimeException("Async event listeners thread pool is not terminated");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// TODO change signature
throw new RuntimeException(e);
}
if (pipeDispatcher != null) {
try {
pipeDispatcher.waitForCompletion(timeout);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
@Override
public void addEventListener(EventListenerDescriptor listener) {
listenerDescriptors.add(listener);
log.debug("Registered event listener: " + listener.getName());
}
public void addEventPipe(EventPipeDescriptor pipeDescriptor) {
registeredPipes.addContribution(pipeDescriptor);
log.debug("Registered event pipe: " + pipeDescriptor.getName());
}
public void addEventDispatcher(EventDispatcherDescriptor dispatcherDescriptor) {
dispatchers.addContrib(dispatcherDescriptor);
log.debug("Registered event dispatcher: " + dispatcherDescriptor.getName());
}
@Override
public void removeEventListener(EventListenerDescriptor listener) {
listenerDescriptors.removeDescriptor(listener);
log.debug("Unregistered event listener: " + listener.getName());
}
public void removeEventPipe(EventPipeDescriptor pipeDescriptor) {
registeredPipes.removeContribution(pipeDescriptor);
log.debug("Unregistered event pipe: " + pipeDescriptor.getName());
}
public void removeEventDispatcher(EventDispatcherDescriptor dispatcherDescriptor) {
dispatchers.removeContrib(dispatcherDescriptor);
log.debug("Unregistered event dispatcher: " + dispatcherDescriptor.getName());
}
@Override
public void fireEvent(String name, EventContext context) {
fireEvent(new EventImpl(name, context));
}
@Override
public void fireEvent(Event event) {
String ename = event.getName();
EventStats stats = Framework.getService(EventStats.class);
for (EventListenerDescriptor desc : listenerDescriptors.getEnabledInlineListenersDescriptors()) {
if (!desc.acceptEvent(ename)) {
continue;
}
try {
long t0 = System.currentTimeMillis();
SequenceTracer.start("Fire sync event " + event.getName());
desc.asEventListener().handleEvent(event);
long elapsed = System.currentTimeMillis() - t0;
SequenceTracer.stop("done in " + elapsed + " ms");
if (stats != null) {
stats.logSyncExec(desc, elapsed);
}
if (event.isCanceled()) {
// break loop
return;
}
} catch (RuntimeException e) {
// get message
SequenceTracer.destroy("failure");
String message = "Exception during " + desc.getName() + " sync listener execution, ";
if (event.isBubbleException()) {
message += "other listeners will be ignored";
} else if (event.isMarkedForRollBack()) {
message += "transaction will be rolled back";
if (event.getRollbackMessage() != null) {
message += " (" + event.getRollbackMessage() + ")";
}
} else {
message += "continuing to run other listeners";
}
// log
if (e instanceof RecoverableClientException) {
log.info(message + "\n" + e.getMessage());
log.debug(message, e);
} else {
log.error(message, e);
}
// rethrow or swallow
if (event.isBubbleException()) {
throw e;
} else if (event.isMarkedForRollBack()) {
Exception ee;
if (event.getRollbackException() != null) {
ee = event.getRollbackException();
} else {
ee = e;
}
// when marked for rollback, throw a generic
// RuntimeException to make sure nobody catches it
throw new RuntimeException(message, ee);
} else {
// swallow exception
}
}
}
if (!event.isInline()) { // record the event
// don't record the complete event, only a shallow copy
ShallowEvent shallowEvent = ShallowEvent.create(event);
if (event.isImmediate()) {
EventBundleImpl b = new EventBundleImpl();
b.push(shallowEvent);
fireEventBundle(b);
} else {
recordEvent(shallowEvent);
}
}
}
@Override
public void fireEventBundle(EventBundle event) {
boolean comesFromJMS = false;
if (event instanceof ReconnectedEventBundle) {
if (((ReconnectedEventBundle) event).comesFromJMS()) {
comesFromJMS = true;
}
}
List<EventListenerDescriptor> postCommitSync = listenerDescriptors.getEnabledSyncPostCommitListenersDescriptors();
List<EventListenerDescriptor> postCommitAsync = listenerDescriptors.getEnabledAsyncPostCommitListenersDescriptors();
if (bulkModeEnabled) {
// run all listeners synchronously in one transaction
List<EventListenerDescriptor> listeners = new ArrayList<>();
if (!blockSyncPostCommitProcessing) {
listeners = postCommitSync;
}
if (!blockAsyncProcessing) {
listeners.addAll(postCommitAsync);
}
if (!listeners.isEmpty()) {
postCommitExec.runBulk(listeners, event);
}
return;
}
// run sync listeners
if (blockSyncPostCommitProcessing) {
log.debug("Dropping PostCommit handler execution");
} else if (comesFromJMS) {
// when called from JMS we must skip sync listeners
// - postComit listeners should be on the core
// - there is no transaction started by JMS listener
log.debug("Deactivating sync post-commit listener since we are called from JMS");
} else {
if (!postCommitSync.isEmpty()) {
postCommitExec.run(postCommitSync, event);
}
}
if (blockAsyncProcessing) {
log.debug("Dopping bundle");
return;
}
// fire async listeners
if (AsyncProcessorConfig.forceJMSUsage() && !comesFromJMS) {
log.debug("Skipping async exec, this will be triggered via JMS");
} else {
if (pipeDispatcher == null) {
asyncExec.run(postCommitAsync, event);
} else {
// rather than sending to the WorkManager: send to the Pipe
pipeDispatcher.sendEventBundle(event);
}
}
}
@Override
public void fireEventBundleSync(EventBundle event) {
for (EventListenerDescriptor desc : listenerDescriptors.getEnabledSyncPostCommitListenersDescriptors()) {
desc.asPostCommitListener().handleEvent(event);
}
for (EventListenerDescriptor desc : listenerDescriptors.getEnabledAsyncPostCommitListenersDescriptors()) {
desc.asPostCommitListener().handleEvent(event);
}
}
@Override
public List<EventListener> getEventListeners() {
return listenerDescriptors.getInLineListeners();
}
@Override
public List<PostCommitEventListener> getPostCommitEventListeners() {
List<PostCommitEventListener> result = new ArrayList<>();
result.addAll(listenerDescriptors.getSyncPostCommitListeners());
result.addAll(listenerDescriptors.getAsyncPostCommitListeners());
return result;
}
public EventListenerList getEventListenerList() {
return listenerDescriptors;
}
@Override
public EventListenerDescriptor getEventListener(String name) {
return listenerDescriptors.getDescriptor(name);
}
// methods for monitoring
@Override
public EventListenerList getListenerList() {
return listenerDescriptors;
}
@Override
public void setListenerEnabledFlag(String listenerName, boolean enabled) {
if (!listenerDescriptors.hasListener(listenerName)) {
return;
}
for (EventListenerDescriptor desc : listenerDescriptors.getAsyncPostCommitListenersDescriptors()) {
if (desc.getName().equals(listenerName)) {
desc.setEnabled(enabled);
synchronized (this) {
listenerDescriptors.recomputeEnabledListeners();
}
return;
}
}
for (EventListenerDescriptor desc : listenerDescriptors.getSyncPostCommitListenersDescriptors()) {
if (desc.getName().equals(listenerName)) {
desc.setEnabled(enabled);
synchronized (this) {
listenerDescriptors.recomputeEnabledListeners();
}
return;
}
}
for (EventListenerDescriptor desc : listenerDescriptors.getInlineListenersDescriptors()) {
if (desc.getName().equals(listenerName)) {
desc.setEnabled(enabled);
synchronized (this) {
listenerDescriptors.recomputeEnabledListeners();
}
return;
}
}
}
@Override
public int getActiveThreadsCount() {
return asyncExec.getActiveCount();
}
@Override
public int getEventsInQueueCount() {
return asyncExec.getUnfinishedCount();
}
@Override
public boolean isBlockAsyncHandlers() {
return blockAsyncProcessing;
}
@Override
public boolean isBlockSyncPostCommitHandlers() {
return blockSyncPostCommitProcessing;
}
@Override
public void setBlockAsyncHandlers(boolean blockAsyncHandlers) {
blockAsyncProcessing = blockAsyncHandlers;
}
@Override
public void setBlockSyncPostCommitHandlers(boolean blockSyncPostComitHandlers) {
blockSyncPostCommitProcessing = blockSyncPostComitHandlers;
}
@Override
public boolean isBulkModeEnabled() {
return bulkModeEnabled;
}
@Override
public void setBulkModeEnabled(boolean bulkModeEnabled) {
this.bulkModeEnabled = bulkModeEnabled;
}
protected void recordEvent(Event event) {
CompositeEventBundle b = threadBundles.get();
b.push(event);
if (TransactionHelper.isTransactionActive()) {
if (!b.registeredSynchronization) {
// register as synchronization
try {
TransactionHelper.lookupTransactionManager().getTransaction().registerSynchronization(this);
} catch (NamingException | SystemException | RollbackException e) {
throw new RuntimeException("Cannot register Synchronization", e);
}
b.registeredSynchronization = true;
}
} else if (event.isCommitEvent()) {
handleTxCommited();
}
}
@Override
public void beforeCompletion() {
}
@Override
public void afterCompletion(int status) {
if (status == Status.STATUS_COMMITTED) {
handleTxCommited();
} else if (status == Status.STATUS_ROLLEDBACK) {
handleTxRollbacked();
} else {
log.error("Unexpected afterCompletion status: " + status);
}
}
protected void handleTxRollbacked() {
threadBundles.remove();
}
protected void handleTxCommited() {
CompositeEventBundle b = threadBundles.get();
threadBundles.remove();
// notify post commit event listeners
for (EventBundle bundle : b.byRepository.values()) {
try {
fireEventBundle(bundle);
} catch (NuxeoException e) {
log.error("Error while processing " + bundle, e);
}
}
}
}