/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.gui.workflow; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchListener; import de.rcenvironment.core.component.api.ComponentConstants; import de.rcenvironment.core.component.execution.api.ComponentExecutionInformation; import de.rcenvironment.core.component.execution.api.ComponentExecutionService; import de.rcenvironment.core.component.execution.api.ComponentState; import de.rcenvironment.core.component.execution.api.ConsoleRow; import de.rcenvironment.core.component.execution.api.ConsoleRowUtils; import de.rcenvironment.core.component.execution.api.ConsoleRow.Type; import de.rcenvironment.core.component.execution.api.ExecutionControllerException; import de.rcenvironment.core.component.workflow.api.WorkflowConstants; import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionInformation; import de.rcenvironment.core.component.workflow.execution.api.WorkflowExecutionService; import de.rcenvironment.core.component.workflow.execution.api.WorkflowState; import de.rcenvironment.core.component.workflow.execution.api.WorkflowStateNotificationSubscriber; import de.rcenvironment.core.component.workflow.execution.spi.SingleWorkflowStateChangeListener; import de.rcenvironment.core.notification.DefaultNotificationSubscriber; import de.rcenvironment.core.notification.DistributedNotificationService; import de.rcenvironment.core.notification.Notification; import de.rcenvironment.core.notification.NotificationSubscriber; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.core.utils.common.StringUtils; import de.rcenvironment.core.utils.common.rpc.RemoteOperationException; import de.rcenvironment.core.utils.incubator.ServiceRegistry; import de.rcenvironment.core.utils.incubator.ServiceRegistryAccess; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncExceptionListener; import de.rcenvironment.toolkit.modules.concurrency.api.CallablesGroup; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Prevents the Workbench to be closed as long as there are active workflows. * * @author Christian Weiss * @author Doreen Seider */ final class ActiveWorkflowShutdownListener implements IWorkbenchListener { private static final String WORKFLOW_HANDLE_ERROR = "Failed to handle active workflows during shutdown"; private static final Log LOGGER = LogFactory.getLog(ActiveWorkflowShutdownListener.class); /** * In case active workflows exist the user is presented a dialog to confirm the disposal. * * @see org.eclipse.ui.IWorkbenchListener#preShutdown(org.eclipse.ui.IWorkbench, boolean) */ @Override public boolean preShutdown(final IWorkbench workbench, final boolean forced) { boolean shutdown = true; final Map<String, WorkflowState> wfStates = new HashMap<>(); final Map<String, ComponentState> compStates = new HashMap<>(); ServiceRegistryAccess serviceRegistryAccess = ServiceRegistry.createAccessFor(this); final WorkflowExecutionService wfExeService = serviceRegistryAccess.getService(WorkflowExecutionService.class); // Note: it is worked on snapshots. newly created workflow or component instances will not be considered. this needs to be improved // by adding support for kind of graceful shutdown final Set<WorkflowExecutionInformation> localWfExeInfosSnapshot = wfExeService.getLocalWorkflowExecutionInformations(); final Set<WorkflowExecutionInformation> localActiveWfExeInfosSnapshot = getActiveWorkflows(localWfExeInfosSnapshot, wfStates); final ComponentExecutionService compExeService = serviceRegistryAccess.getService(ComponentExecutionService.class); final Set<ComponentExecutionInformation> localCompExeInfosSnapshot = compExeService.getLocalComponentExecutionInformations(); final Set<ComponentExecutionInformation> localActiveCompExeInfosSnapshot = getActiveComponents(compExeService, localCompExeInfosSnapshot, compStates); boolean wfOrCompActive = localActiveWfExeInfosSnapshot.size() > 0 || localActiveCompExeInfosSnapshot.size() > 0; try { if (!forced && wfOrCompActive) { final int maxLines = 15; int lines = 0; Set<String> activeWfExeIds = new HashSet<>(); String message = "\n"; message += "\nWorkflows:\n"; for (WorkflowExecutionInformation wfExeInfo : localActiveWfExeInfosSnapshot) { message += StringUtils.format("- %s -> %s\n", wfExeInfo.getInstanceName(), wfStates.get(wfExeInfo.getExecutionIdentifier()).getDisplayName()); lines++; activeWfExeIds.add(wfExeInfo.getExecutionIdentifier()); if (lines > 10) { message += "...\n"; break; } } if (lines == 0) { message += "-\n"; } int workflowLines = lines; message += "\nComponents:\n"; for (ComponentExecutionInformation compExeInfo : localActiveCompExeInfosSnapshot) { if (!activeWfExeIds.contains(compExeInfo.getWorkflowExecutionIdentifier())) { message += StringUtils.format("- %s (%s) -> %s\n", compExeInfo.getInstanceName(), compExeInfo.getWorkflowInstanceName(), compStates.get(compExeInfo.getExecutionIdentifier()).getDisplayName()); lines++; if (lines > maxLines) { message += "...\n"; break; } } } if (workflowLines == lines) { message += "-\n"; } shutdown = MessageDialog.openQuestion(workbench.getActiveWorkbenchWindow().getShell(), Messages.activeWorkflowsTitle, Messages.activeWorkflowsMessage + message); } } catch (IllegalStateException e) { LOGGER.error(WORKFLOW_HANDLE_ERROR, e); } if (shutdown && wfOrCompActive) { final DistributedNotificationService notificationService = serviceRegistryAccess .getService(DistributedNotificationService.class); Job job = new Job("Cancel and dispose all active workflows") { @Override protected IStatus run(IProgressMonitor monitor) { List<String> cancelledDisposedWfExeIds = new ArrayList<>(); CallablesGroup<Void> callablesGroup = ConcurrencyUtils.getFactory().createCallablesGroup(Void.class); for (WorkflowExecutionInformation wfExeInfo : localWfExeInfosSnapshot) { final WorkflowExecutionInformation finalWfExeInfo = wfExeInfo; if (!cancelledDisposedWfExeIds.contains(finalWfExeInfo.getExecutionIdentifier())) { callablesGroup.add(new Callable<Void>() { @Override @TaskDescription("Call method of workflow component") public Void call() throws Exception { cancelAndDisposeWorkflow(wfExeService, notificationService, finalWfExeInfo); return null; } }, "Cancel/dispose workflow: " + finalWfExeInfo.getExecutionIdentifier()); cancelledDisposedWfExeIds.add(finalWfExeInfo.getExecutionIdentifier()); } } // as it causes errors, currently remote workflows with local active component are not cancelled (as it was before). Due // to heartbeat sending, the remote workflow controller will recognize the missing components and will fail. That is an // improvement in 6.0.0 compared to 5.2.1 -seid_do // for (ComponentExecutionInformation compExeInfo : localCompExeInfosSnapshot) { // final ComponentExecutionInformation finalCompExeInfo = compExeInfo; // if (!cancelledDisposedWfExeIds.contains(finalCompExeInfo.getWorkflowExecutionIdentifier())) { // callablesGroup.add(new Callable<Void>() { // // @Override // @TaskDescription("Call method of workflow component") // public Void call() throws Exception { // cancelAndDisposeWorkflow(wfExeService, notificationService, // finalCompExeInfo.getWorkflowExecutionIdentifier(), // finalCompExeInfo.getWorkflowNodeId(), finalCompExeInfo.getWorkflowInstanceName(), // wfStates.get(finalCompExeInfo.getWorkflowExecutionIdentifier())); // return null; // } // }, "Cancel/dispose workflow: " + finalCompExeInfo.getWorkflowExecutionIdentifier()); // cancelledDisposedWfExeIds.add(finalCompExeInfo.getWorkflowExecutionIdentifier()); // } // } callablesGroup.executeParallel(new AsyncExceptionListener() { @Override public void onAsyncException(Exception e) { LOGGER.error("Failed to cancel/dispose workflow", e); } }); return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { return family == UncompletedJobsShutdownListener.MUST_BE_COMPLETED_ON_SHUTDOWN_JOB_FAMILY; } }; job.setSystem(true); job.schedule(); } return shutdown; } private void cancelAndDisposeWorkflow(final WorkflowExecutionService workflowExecutionService, final DistributedNotificationService notificationService, final WorkflowExecutionInformation wfExeInfo) { final CountDownLatch wfDisposedLatch = new CountDownLatch(2); WorkflowStateNotificationSubscriber workflowStateChangeListener = new WorkflowStateNotificationSubscriber(new SingleWorkflowStateChangeListener() { @Override public void onWorkflowStateChanged(WorkflowState newState) { LOGGER.debug(StringUtils.format("Received state change event for workflow %s: ", wfExeInfo.getExecutionIdentifier(), newState.getDisplayName())); switch (newState) { case CANCELLED: case FAILED: case RESULTS_REJECTED: case FINISHED: try { workflowExecutionService.dispose(wfExeInfo.getExecutionIdentifier(), wfExeInfo.getNodeId()); } catch (ExecutionControllerException | RemoteOperationException e) { LOGGER.error(StringUtils.format("Failed to dispose workflow '%s' (%s)", wfExeInfo.getInstanceName(), wfExeInfo.getExecutionIdentifier()), e); wfDisposedLatch.countDown(); } break; case DISPOSED: wfDisposedLatch.countDown(); break; default: break; } } @Override public void onWorkflowNotAliveAnymore(String errorMessage) { LOGGER.error(StringUtils.format("Failed to dispose workflow '%s' (%s) - %s", wfExeInfo.getInstanceName(), wfExeInfo.getExecutionIdentifier(), errorMessage)); wfDisposedLatch.countDown(); } }, wfExeInfo.getExecutionIdentifier()); try { notificationService.subscribe(WorkflowConstants.STATE_NOTIFICATION_ID + wfExeInfo.getExecutionIdentifier(), workflowStateChangeListener, wfExeInfo.getNodeId()); notificationService.subscribe( ConsoleRowUtils.composeConsoleNotificationId(wfExeInfo.getNodeId(), wfExeInfo.getExecutionIdentifier()), new ConsoleRowSubscriber(wfDisposedLatch), wfExeInfo.getNodeId()); if (!WorkflowConstants.FINAL_WORKFLOW_STATES.contains(wfExeInfo.getWorkflowState())) { workflowExecutionService.cancel(wfExeInfo.getExecutionIdentifier(), wfExeInfo.getNodeId()); } else { workflowExecutionService.dispose(wfExeInfo.getExecutionIdentifier(), wfExeInfo.getNodeId()); } } catch (ExecutionControllerException | RemoteOperationException e) { LOGGER.error(StringUtils.format("Failed to cancel/dispose workflow '%s' (%s): %s", wfExeInfo.getExecutionIdentifier(), wfExeInfo.getExecutionIdentifier(), e.getMessage())); } try { wfDisposedLatch.await(); } catch (InterruptedException e) { LOGGER.debug(StringUtils.format("Was interupted when cancelling/disposing workflow '%s' (%s)", wfExeInfo.getInstanceName(), wfExeInfo.getExecutionIdentifier()), e); Thread.currentThread().interrupt(); } } private Set<WorkflowExecutionInformation> getActiveWorkflows(Set<WorkflowExecutionInformation> localWfExeInfos, Map<String, WorkflowState> wfStates) { Set<WorkflowExecutionInformation> activeWfExeInfos = new HashSet<>(); Iterator<WorkflowExecutionInformation> iterator = localWfExeInfos.iterator(); while (iterator.hasNext()) { WorkflowExecutionInformation wfExeInfo = iterator.next(); WorkflowState state = wfExeInfo.getWorkflowState(); if (!WorkflowConstants.FINAL_WORKFLOW_STATES_WITH_DISPOSED.contains(state)) { activeWfExeInfos.add(wfExeInfo); } if (state == WorkflowState.DISPOSED) { iterator.remove(); } wfStates.put(wfExeInfo.getExecutionIdentifier(), state); } return activeWfExeInfos; } private Set<ComponentExecutionInformation> getActiveComponents(ComponentExecutionService componentExecutionService, Set<ComponentExecutionInformation> localCompExeInfos, Map<String, ComponentState> compStates) { Set<ComponentExecutionInformation> activeCompExeInfos = new HashSet<>(); Iterator<ComponentExecutionInformation> iterator = localCompExeInfos.iterator(); while (iterator.hasNext()) { ComponentExecutionInformation compExeInfo = iterator.next(); try { ComponentState state = componentExecutionService.getComponentState(compExeInfo.getExecutionIdentifier(), compExeInfo.getNodeId()); if (!ComponentConstants.FINAL_COMPONENT_STATES_WITH_DISPOSED.contains(state)) { activeCompExeInfos.add(compExeInfo); } if (state == ComponentState.DISPOSED) { iterator.remove(); } compStates.put(compExeInfo.getExecutionIdentifier(), state); } catch (RemoteOperationException | ExecutionControllerException e) { LOGGER.error(StringUtils.format("Failed to get state for component '%s' (%s); cause: %s", compExeInfo.getInstanceName(), compExeInfo.getExecutionIdentifier(), e.toString())); } } return activeCompExeInfos; } @Override public void postShutdown(final IWorkbench workbench) {} /** * Listens for workflow disposed status on console row level. * * @author Doreen Seider */ private static class ConsoleRowSubscriber extends DefaultNotificationSubscriber { private static final long serialVersionUID = 6177970783784847691L; private final transient CountDownLatch wfDisposeLatch; ConsoleRowSubscriber(CountDownLatch countDownLatch) { this.wfDisposeLatch = countDownLatch; } @Override public Class<?> getInterface() { return NotificationSubscriber.class; } @Override protected void processNotification(Notification notification) { Serializable body = notification.getBody(); if (!(notification.getBody() instanceof ConsoleRow)) { LOGGER.error("Unexpected notification type on ConsoleRow channel: body class is " + body.getClass()); return; } ConsoleRow row = (ConsoleRow) body; if (row.getType() == Type.LIFE_CYCLE_EVENT) { LOGGER.debug("Received workflow life-cycle event: " + row.getPayload()); if (row.getPayload().startsWith(ConsoleRow.WorkflowLifecyleEventType.NEW_STATE.name()) && row.getPayload().endsWith(WorkflowState.DISPOSED.name())) { wfDisposeLatch.countDown(); } } } } }