/* * Copyright (c) 2006-2013 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Bogdan Stefanescu * Thierry Delprat * Florent Guillaume */ package org.nuxeo.ecm.core.event.impl; import java.rmi.dgc.VMID; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; 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.ecm.core.api.ClientException; 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.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<String, EventBundle>(); 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<AsyncWaitHook>(); protected boolean blockAsyncProcessing = false; protected boolean blockSyncPostCommitProcessing = false; protected boolean bulkModeEnabled = false; public EventServiceImpl() { listenerDescriptors = new EventListenerList(); postCommitExec = new PostCommitEventExecutor(); asyncExec = new AsyncEventExecutor(); } public void init() { asyncExec.init(); } public void shutdown(long timeoutMillis) throws InterruptedException { postCommitExec.shutdown(timeoutMillis); Set<AsyncWaitHook> notTerminated = new HashSet<AsyncWaitHook>(); for (AsyncWaitHook hook : asyncWaitHooks) { if (hook.shutdown() == false) { notTerminated.add(hook); } } if (!notTerminated.isEmpty()) { throw new RuntimeException("Asynch services are still running : " + notTerminated); } if (asyncExec.shutdown(timeoutMillis) == false) { throw new RuntimeException( "Async executor is still running, timeout expired"); } } public void registerForAsyncWait(AsyncWaitHook callback) { asyncWaitHooks.add(callback); } public void unregisterForAsyncWait(AsyncWaitHook callback) { asyncWaitHooks.remove(callback); } /** * @deprecated use {@link #waitForAsyncCompletion()} instead. */ @Deprecated public int getActiveAsyncTaskCount() { return asyncExec.getUnfinishedCount(); } @Override public void waitForAsyncCompletion() { waitForAsyncCompletion(Long.MAX_VALUE); } @Override public void waitForAsyncCompletion(long timeout) { Set<AsyncWaitHook> notCompleted = new HashSet<AsyncWaitHook>(); for (AsyncWaitHook hook : asyncWaitHooks) { if (!hook.waitForAsyncCompletion()) { notCompleted.add(hook); } } 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); } } @Override public void addEventListener(EventListenerDescriptor listener) { try { listenerDescriptors.add(listener); log.debug("Registered event listener: " + listener.getName()); } catch (Exception e) { log.error( "Failed to register event listener: " + listener.getName(), e); } } @Override public void removeEventListener(EventListenerDescriptor listener) { try { listenerDescriptors.removeDescriptor(listener); log.debug("Unregistered event listener: " + listener.getName()); } catch (Exception e) { log.error( "Failed to unregister event listener: " + listener.getName(), e); } } protected EventStats getEventStats() { try { return Framework.getService(EventStats.class); } catch (Exception e) { log.warn("Failed to lookup event stats service", e); } return null; } @Override public void fireEvent(String name, EventContext context) throws ClientException { fireEvent(new EventImpl(name, context)); } @Override public void fireEvent(Event event) throws ClientException { String ename = event.getName(); EventStats stats = getEventStats(); for (EventListenerDescriptor desc : listenerDescriptors.getEnabledInlineListenersDescriptors()) { if (desc.acceptEvent(ename)) { Throwable rollbackException = null; try { long t0 = System.currentTimeMillis(); desc.asEventListener().handleEvent(event); if (stats != null) { stats.logSyncExec(desc, System.currentTimeMillis() - t0); } } catch (Throwable t) { String message; if (event.isBubbleException() || event.isMarkedForRollBack()) { message = "Error during " + desc.getName() + " sync listener execution, transaction will be rolled back"; rollbackException = t; } else { message = "Error during " + desc.getName() + " sync listener execution, transaction won't be rolled back " + "since event.markRollBack() was not called by the Listener"; } if (t instanceof RecoverableClientException) { log.info(message + "\n" + t.getMessage()); log.debug(message, t); } else { log.error(message, t); } } finally { if (event.isBubbleException()) { throw new RuntimeException(rollbackException); } else if (event.isMarkedForRollBack()) { String message = "Exception during " + desc.getName() + " sync listener execution, rolling back"; if (event.getRollbackMessage() != null) { message = message + " (" + event.getRollbackMessage() + ")"; } if (event.getRollbackException() != null) { rollbackException = event.getRollbackException(); } if (rollbackException != null) { throw new RuntimeException(message, rollbackException); } else { throw new RuntimeException(message); } } if (event.isCanceled()) { return; } } } } 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) throws ClientException { 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<EventListenerDescriptor>(); 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 { asyncExec.run(postCommitAsync, event); } } @Override public void fireEventBundleSync(EventBundle event) throws ClientException { 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<PostCommitEventListener>(); 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 (ClientException e) { log.error("Error while processing " + bundle, e); } } } }