/* * (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: * Bogdan Stefanescu * Thierry Delprat * Florent Guillaume * Andrei Nechaev */ package org.nuxeo.ecm.core.event.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.ConcurrentUpdateException; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; 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.EventService; import org.nuxeo.ecm.core.event.EventStats; import org.nuxeo.ecm.core.event.ReconnectedEventBundle; import org.nuxeo.ecm.core.work.AbstractWork; import org.nuxeo.ecm.core.work.api.Work.State; import org.nuxeo.ecm.core.work.api.WorkManager; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.transaction.TransactionHelper; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * Executor of async listeners passing them to the WorkManager. */ @SuppressWarnings("PackageAccessibility") public class AsyncEventExecutor { private static final Log log = LogFactory.getLog(AsyncEventExecutor.class); public AsyncEventExecutor() { } public WorkManager getWorkManager() { return Framework.getLocalService(WorkManager.class); } public void init() { WorkManager workManager = getWorkManager(); if (workManager != null) { workManager.init(); } } public boolean shutdown(long timeoutMillis) throws InterruptedException { WorkManager workManager = getWorkManager(); if (workManager == null) { return true; } return workManager.shutdown(timeoutMillis, TimeUnit.MILLISECONDS); } public boolean waitForCompletion(long timeoutMillis) throws InterruptedException { WorkManager workManager = getWorkManager(); if (workManager == null) { return false; } return workManager.awaitCompletion(timeoutMillis, TimeUnit.MILLISECONDS); } public void run(final List<EventListenerDescriptor> listeners, EventBundle bundle) { // EventBundle that have gone through bus have been serialized // we need to reconnect them before filtering // this means we need a valid transaction ! if (!(bundle instanceof ReconnectedEventBundleImpl)) { scheduleListeners(listeners, bundle); } else { final EventBundle tmpBundle = bundle; TransactionHelper.runInTransaction(() -> { EventBundle connectedBundle = new EventBundleImpl(); Map<String, CoreSession> sessions = new HashMap<>(); List<Event> events = ((ReconnectedEventBundleImpl)tmpBundle).getReconnectedEvents(); for (Event event : events) { connectedBundle.push(event); CoreSession session = event.getContext().getCoreSession(); if (!(sessions.keySet().contains(session.getRepositoryName()))) { sessions.put(session.getRepositoryName(), session); } } sessions.values().forEach(CoreSession::close); scheduleListeners(listeners, connectedBundle); }); } } private void scheduleListeners(final List<EventListenerDescriptor> listeners, EventBundle bundle) { for (EventListenerDescriptor listener : listeners) { EventBundle filtered = listener.filterBundle(bundle); if (filtered.isEmpty()) { continue; } // This may be called in a transaction if event.isCommitEvent() is true or at transaction commit // in other cases. If the transaction has been marked rollback-only, then scheduling must discard // so we schedule "after commit" getWorkManager().schedule(new ListenerWork(listener, filtered), true); } } public int getUnfinishedCount() { WorkManager workManager = getWorkManager(); int n = 0; for (String queueId : workManager.getWorkQueueIds()) { n += workManager.getQueueSize(queueId, State.SCHEDULED) + workManager.getQueueSize(queueId, State.RUNNING); } return n; } public int getActiveCount() { WorkManager workManager = getWorkManager(); int n = 0; for (String queueId : workManager.getWorkQueueIds()) { n += workManager.getQueueSize(queueId, State.RUNNING); } return n; } protected static class ListenerWork extends AbstractWork { private static final long serialVersionUID = 1L; private static final int DEFAULT_RETRY_COUNT = 2; protected final String title; protected ReconnectedEventBundle bundle; protected String listenerName; protected int retryCount; protected transient EventListenerDescriptor listener; public ListenerWork(EventListenerDescriptor listener, EventBundle bundle) { super(); // random id, for unique job listenerName = listener.getName(); if (bundle instanceof ReconnectedEventBundle) { this.bundle = (ReconnectedEventBundle) bundle; } else { this.bundle = new ReconnectedEventBundleImpl(bundle, listenerName); } List<String> l = new LinkedList<String>(); List<String> docIds = new LinkedList<String>(); String repositoryName = null; for (Event event : bundle) { String s = event.getName(); EventContext ctx = event.getContext(); if (ctx instanceof DocumentEventContext) { DocumentModel source = ((DocumentEventContext) ctx).getSourceDocument(); if (source != null) { s += "/" + source.getRef(); docIds.add(source.getId()); repositoryName = source.getRepositoryName(); } } l.add(s); } title = "Listener " + listenerName + " " + l; if (!docIds.isEmpty()) { setDocuments(repositoryName, docIds); } Integer count = listener.getRetryCount(); retryCount = count == null ? DEFAULT_RETRY_COUNT : count; if (retryCount < 0) { retryCount = DEFAULT_RETRY_COUNT; } } @Override public String getCategory() { return listenerName; } @Override public String getTitle() { return title; } @Override public int getRetryCount() { return retryCount; } @Override public void work() { EventService eventService = Framework.getLocalService(EventService.class); listener = eventService.getEventListener(listenerName); if (listener == null) { throw new RuntimeException("Cannot find listener: " + listenerName); } listener.asPostCommitListener().handleEvent(bundle); } @Override public void cleanUp(boolean ok, Exception e) { super.cleanUp(ok, e); bundle.disconnect(); if (e != null && !(e instanceof InterruptedException) && !(e instanceof ConcurrentUpdateException)) { log.error("Failed to execute async event " + bundle.getName() + " on listener " + listenerName, e); } if (listener != null) { EventStats stats = Framework.getLocalService(EventStats.class); if (stats != null) { stats.logAsyncExec(listener, System.currentTimeMillis() - getStartTime()); } listener = null; } } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName()); buf.append('('); buf.append(title); buf.append(", "); buf.append(getProgress()); buf.append(", "); buf.append(getStatus()); buf.append(')'); return buf.toString(); } } }