/******************************************************************************* * Copyright (c) 2006, 2013 Wind River Systems 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: * Wind River Systems - initial API and implementation * Nokia - added StepWithProgress. Oct, 2008 *******************************************************************************/ package org.eclipse.cdt.dsf.concurrent; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import org.eclipse.cdt.dsf.concurrent.RequestMonitor.ICanceledListener; import org.eclipse.cdt.dsf.internal.DsfPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubProgressMonitor; /** * Convenience class for implementing a series of commands that need to be * executed asynchronously. * <p> * Certain complex tasks require multiple commands to be executed in a chain, * because for example result of one command is used as input into another * command. The typical DSF pattern of solving this problem is the following: * <li> * <br> 1. original caller passes a RequestMonitor callback to a method and invokes it * <br> 2. the method is executed by a subsystem * <br> 3. when the method is finished it calls another method and passes * the original callback to it * <br> 4. steps 2-3 are repeated a number of times * <br> 5. when the last method in a chain is executed, it submits the original * RequestMonitor callback * </li> * <p> * This pattern is very useful in itself, but it proves very difficult to follow * because the methods can be scattered across many classes and systems. Also * if progress reporting, cancellability, and roll-back ability is required, it * has to be re-implemented every time. The Sequence class tries to address * this problem by containing this pattern in a single class. * * @since 1.0 */ @ThreadSafe abstract public class Sequence extends DsfRunnable implements Future<Object> { /** * The abstract class that each step has to implement. */ abstract public static class Step { private Sequence fSequence; /** * Sets the sequence that this step belongs to. It is only accessible * by the sequence itself, and is not meant to be called by sequence * sub-classes. */ void setSequence(Sequence sequence) { fSequence = sequence; } /** Returns the sequence that this step is running in. */ public Sequence getSequence() { return fSequence; } /** * Executes the step. Overriding classes should perform the * work in this method. * @param rm Result token to submit to executor when step is finished. */ public void execute(RequestMonitor rm) { rm.done(); } /** * Roll back gives the step implementation a chance to undo the * operation that was performed by execute(). * <br> * Note if the {@link #execute(RequestMonitor)} call completes with a * non-OK status, then rollBack will not be called for that step. * Instead it will be called for the previous step. * @param rm Result token to submit to executor when rolling back the step is finished. */ public void rollBack(RequestMonitor rm) { rm.done(); } /** * Returns the number of progress monitor ticks corresponding to this * step. */ public int getTicks() { return 1; } /** * Task name for this step. This will be displayed in the label of the * progress monitor of the owner sequence. * * @return name of the task carried out by the step, can be * <code>null</code>, in which case the overall task name will be used. * * @since 1.1 */ public String getTaskName() { return ""; //$NON-NLS-1$ } } /** * A step that will report execution progress by itself on the progress * monitor of the owner sequence.<br> * <br> * Note we don't offer a rollBack(RequestMonitor, IProgressMonitor) as we * don't want end user to be able to cancel the rollback. * * @since 1.1 */ abstract public static class StepWithProgress extends Step { @Override // don't allow subclass to implement this by "final" it. final public void execute(RequestMonitor rm) { assert false : "execute(RequestMonitor rm, IProgressMonitor pm) should be called instead"; //$NON-NLS-1$ } /** * Execute the step with a progress monitor. Note the given progress * monitor is a sub progress monitor of the owner sequence which is * supposed to be fully controlled by the step. Namely the step should * call beginTask() and done() of the monitor. * * @param rm * @param pm */ public void execute(RequestMonitor rm, IProgressMonitor pm) { rm.done(); pm.done(); } } /** The synchronization object for this future */ final Sync fSync = new Sync(); /** * Executor that this sequence is running in. It is used by the sequence * to submit the runnables for steps, and for submitting the result. */ final private DsfExecutor fExecutor; /** * Result callback to invoke when the sequence is finished. Intended to * be used when the sequence is created and invoked from the executor * thread. Otherwise, the {@link Future#get()} method is the appropriate * method of retrieving the result. */ final private RequestMonitor fRequestMonitor; /** Status indicating the success/failure of the test. Used internally only. */ @ConfinedToDsfExecutor("getExecutor") private IStatus fStatus = Status.OK_STATUS; @ConfinedToDsfExecutor("getExecutor") private int fCurrentStepIdx = 0; /** Task name for this sequence used with the progress monitor */ final private String fTaskName; /** Task name used when the sequence is being rolled back. */ final private String fRollbackTaskName; final private IProgressMonitor fProgressMonitor; /** Convenience constructor with limited arguments. */ public Sequence(DsfExecutor executor) { this(executor, new NullProgressMonitor(), "", "", null); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Creates a sequence with a request monitor. If the client cancels the * request monitor, then the request monitors in the * {@link Step#execute(RequestMonitor)} * implementations will immediately call the cancel listeners to notify. * * @param executor The DSF executor which will be used to invoke all * steps. * @param rm The request monitor which will be invoked when the sequence * is completed. */ public Sequence(DsfExecutor executor, RequestMonitor rm) { this(executor, new NullProgressMonitor(), "", "", rm); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Creates a sequence with a progress monitor. If the progress monitor is * canceled, then request monitors in the * {@link Step#execute(RequestMonitor)} implementations will need to call * rm.isCanceled() to discover the cancellation. * @param executor The DSF executor which will be used to invoke all * steps. * @param pm Progress monitor for monitoring this sequence. * @param taskName Name that will be used in call to * {@link IProgressMonitor#beginTask(String, int)},when the task is * started. * @param rollbackTaskName Name that will be used in call to * {@link IProgressMonitor#subTask(String)} if the task is canceled or * aborted. * * @since 1.1 */ public Sequence(DsfExecutor executor, IProgressMonitor pm, String taskName, String rollbackTaskName) { this(executor, pm, taskName, rollbackTaskName, new RequestMonitorWithProgress(ImmediateExecutor.getInstance(), pm)); } /** * Creates a sequence with a request monitor that includes a progress * monitor. * @param executor The DSF executor which will be used to invoke all * steps. * @param rm The request monitor containing the progress monitor * @param taskName Name that will be used in call to * {@link IProgressMonitor#beginTask(String, int)},when the task is * started. * @param rollbackTaskName Name that will be used in call to * {@link IProgressMonitor#subTask(String)} if the task is canceled or * aborted. * * @since 1.1 */ public Sequence(DsfExecutor executor, RequestMonitorWithProgress rm, String taskName, String rollbackTaskName) { this(executor, rm.getProgressMonitor(), taskName, rollbackTaskName, rm); } /** * Constructor that initialized the steps and the result callback. * <p>Note: This constructor should not be used because it creates a * potential ambiguity when one of the two monitors is canceled.</p> * * @param executor The DSF executor which will be used to invoke all * steps. * @param pm Progress monitor for monitoring this sequence. This * parameter cannot be null. * @param taskName Name that will be used in call to * {@link IProgressMonitor#beginTask(String, int)},when the task is * started. * @param rollbackTaskName Name that will be used in call to * {@link IProgressMonitor#subTask(String)} if the task is canceled or * aborted. * @param Result that will be submitted to executor when sequence is * finished. Can be null if calling from non-executor thread and using * {@link Future#get()} method to wait for the sequence result. */ private Sequence(DsfExecutor executor, IProgressMonitor pm, String taskName, String rollbackTaskName, RequestMonitor rm) { fExecutor = executor; fProgressMonitor = pm; fTaskName = taskName; fRollbackTaskName = rollbackTaskName; fRequestMonitor = rm; if (fRequestMonitor != null) { fRequestMonitor.addCancelListener(new ICanceledListener() { @Override public void requestCanceled(RequestMonitor rm) { fSync.doCancel(); } }); } } /** * Returns the steps to be executed. It is up to the deriving class to * supply the steps and to ensure that the list of steps will not be * modified after the sequence is constructed. * <p> * Steps are purposely not accepted as part of the DsfConstructor, in * order to allow deriving classes to create the steps as a field. And a * setSteps() method is not provided, to guarantee that the steps will not * be modified once set (perhaps this is a bit paranoid, but oh well). */ abstract public Step[] getSteps(); /** Returns the DSF executor for this sequence */ public DsfExecutor getExecutor() { return fExecutor; } /** * Returns the RequestMonitor callback that is registered with the Sequence */ public RequestMonitor getRequestMonitor() { return fRequestMonitor; } /** * The get method blocks until sequence is complete, but always returns null. * @see java.concurrent.Future#get */ @Override public Object get() throws InterruptedException, ExecutionException { fSync.doGet(); return null; } /** * The get method blocks until sequence is complete or until timeout is * reached, but always returns null. * @see java.concurrent.Future#get */ @Override public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { fSync.doGet(unit.toNanos(timeout)); return null; } /** * Don't try to interrupt the DSF executor thread, just ignore the request * if set. * <p>If a request monitor was specified when creating a sequence, that * request monitor will be canceled by this method as well. The client * can also use the request monitor's cancel method to cancel the sequence. * * @see RequestMonitor#cancel() */ @Override public boolean cancel(boolean mayInterruptIfRunning) { // Cancel the request monitor first, to avoid a situation where // the request monitor is not canceled but the status is set // to canceled. if (fRequestMonitor != null) { fRequestMonitor.cancel(); } return fSync.doCancel(); } @Override public boolean isCancelled() { return fSync.doIsCancelled(); } @Override public boolean isDone() { return fSync.doIsDone(); } @Override public void run() { // Change the state to running. if (fSync.doRun()) { // Set the reference to this sequence in each step. int totalTicks = 0; for (Step step : getSteps()) { step.setSequence(this); totalTicks += step.getTicks(); } // Set the task name if (fTaskName != null) { fProgressMonitor.beginTask(fTaskName, totalTicks); } // Call the first step executeStep(0); } else { fSync.doFinish(); } } /** * To be called only by the step implementation, Tells the sequence to * submit the next step. */ private void executeStep(int nextStepIndex) { /* * At end of each step check progress monitor to see if it's cancelled. * If progress monitor is cancelled, mark the whole sequence as * cancelled. */ if (fProgressMonitor.isCanceled()) { cancel(false); } /* * If sequence was cancelled during last step (or before the sequence * was ever executed), start rolling back the execution. */ if (isCancelled()) { cancelExecution(); return; } /* * Check if we've reached the last step. Note that if execution was * cancelled during the last step (and thus the sequence is * technically finished, since it was cancelled it will be rolled * back. */ if (nextStepIndex >= getSteps().length) { finish(); return; } // Proceed with executing next step. fCurrentStepIdx = nextStepIndex; try { Step currentStep = getSteps()[fCurrentStepIdx]; final boolean stepControlsProgress = (currentStep instanceof StepWithProgress); RequestMonitor rm = new RequestMonitor(fExecutor, fRequestMonitor) { final private int fStepIdx = fCurrentStepIdx; @Override public void handleSuccess() { // Check if we're still the correct step. assert fStepIdx == fCurrentStepIdx; if (!stepControlsProgress) { // then sequence handles the progress report. fProgressMonitor.worked(getSteps()[fStepIdx].getTicks()); } executeStep(fStepIdx + 1); } @Override protected void handleCancel() { Sequence.this.cancel(false); cancelExecution(); }; @Override protected void handleErrorOrWarning() { abortExecution(getStatus(), true); }; @Override protected void handleRejectedExecutionException() { abortExecution( new Status(IStatus.ERROR, DsfPlugin.PLUGIN_ID, 0, "Executor shut down while executing Sequence " + this + ", step #" + fCurrentStepIdx, //$NON-NLS-1$ //$NON-NLS-2$ null), false); } @Override public String toString() { return "Sequence \"" + fTaskName + "\", result for executing step #" + fStepIdx + " = " + getStatus(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } }; fProgressMonitor.subTask(currentStep.getTaskName()); if (stepControlsProgress) { // Create a sub-monitor that will be controlled by the step. SubProgressMonitor subMon = new SubProgressMonitor(fProgressMonitor, currentStep.getTicks(), SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK); ((StepWithProgress) currentStep).execute(rm, subMon); } else { // regular Step currentStep.execute(rm); } } catch (Throwable t) { /* * Catching the exception here will only work if the exception * happens within the execute method. It will not work in cases * when the execute submits other runnables, and the other runnables * encounter the exception. */ abortExecution( new Status(IStatus.ERROR, DsfPlugin.PLUGIN_ID, 0, "Unhandled exception when executing Sequence " + this + ", step #" + fCurrentStepIdx, //$NON-NLS-1$ //$NON-NLS-2$ t), true); /* * Since we caught the exception, it will not be logged by * DefaultDsfExecutable.afterExecution(). So log it here. */ DefaultDsfExecutor.logException(t); } } /** * To be called only by the step implementation. Tells the sequence to * roll back next step. */ private void rollBackStep(int stepIdx) { // If we reach before step 0, finish roll back. if (stepIdx < 0) { finish(); return; } // Proceed with rolling back given step. fCurrentStepIdx = stepIdx; try { getSteps()[fCurrentStepIdx].rollBack(new RequestMonitor(fExecutor, null) { final private int fStepIdx = fCurrentStepIdx; @Override public void handleCompleted() { // Check if we're still the correct step. assert fStepIdx == fCurrentStepIdx; // Proceed to the next step. if (isSuccess()) { // NOTE: The getTicks() is ticks for executing the step, // not for rollBack, // though it does not really hurt to use it here. fProgressMonitor.worked(getSteps()[fStepIdx].getTicks()); rollBackStep(fStepIdx - 1); } else { abortRollBack(getStatus()); } } @Override public String toString() { return "Sequence \"" + fTaskName + "\", result for rolling back step #" + fStepIdx + " = " + getStatus(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } }); } catch(Throwable t) { /* * Catching the exception here will only work if the exception * happens within the execute method. It will not work in cases * when the execute submits other runnables, and the other runnables * encounter the exception. */ abortRollBack(new Status( IStatus.ERROR, DsfPlugin.PLUGIN_ID, 0, "Unhandled exception when rolling back Sequence " + this + ", step #" + fCurrentStepIdx, //$NON-NLS-1$ //$NON-NLS-2$ t)); /* * Since we caught the exception, it will not be logged by * DefaultDsfExecutable.afterExecution(). So log it here. */ DefaultDsfExecutor.logException(t); } } /** * Tells the sequence that its execution is to be aborted and it * should start rolling back the sequence as if it was cancelled by user. */ private void cancelExecution() { if (fRollbackTaskName != null) { fProgressMonitor.subTask(fRollbackTaskName); } fStatus = new Status(IStatus.CANCEL, DsfPlugin.PLUGIN_ID, -1, "Sequence \"" + fTaskName + "\" cancelled.", null); //$NON-NLS-1$ //$NON-NLS-2$ if (fRequestMonitor != null) { fRequestMonitor.setStatus(fStatus); } /* * No need to call fSync, it should have been taken care of by * Future#cancel method. * * Note that we're rolling back starting with the current step, * because the current step was fully executed. This is unlike * abortExecution() where the current step caused the roll-back. */ rollBackStep(fCurrentStepIdx); } /** * Tells the sequence that its execution is to be aborted and it * should start rolling back the sequence as if it was cancelled by user. * * @param status Status to use for reporting the error. * @param rollBack Whether to start rolling back the sequence after abort. * If this parameter is <code>false</code> then the sequence will also * finish. */ private void abortExecution(final IStatus error, boolean rollBack) { if (fRollbackTaskName != null) { fProgressMonitor.subTask(fRollbackTaskName); } fStatus = error; if (fRequestMonitor != null) { fRequestMonitor.setStatus(error); } fSync.doAbort(new CoreException(error)); if (rollBack) { // Roll back starting with previous step, since current step failed. rollBackStep(fCurrentStepIdx - 1); } else { finish(); } } /** * Tells the sequence that that is rolling back, to abort roll back, and * notify the clients. */ private void abortRollBack(final IStatus error) { if (fRollbackTaskName != null) { fProgressMonitor.subTask(fRollbackTaskName); } /* * Compose new status based on previous status information and new * error information. */ MultiStatus newStatus = new MultiStatus(DsfPlugin.PLUGIN_ID, error.getCode(), "Sequence \"" + fTaskName + "\" failed while rolling back.", null); //$NON-NLS-1$ //$NON-NLS-2$ newStatus.merge(error); newStatus.merge(fStatus); fStatus = newStatus; if (fRequestMonitor != null) { fRequestMonitor.setStatus(newStatus); } finish(); } private void finish() { if (fRequestMonitor != null) fRequestMonitor.done(); fSync.doFinish(); } @SuppressWarnings("serial") final class Sync extends AbstractQueuedSynchronizer { private static final int STATE_RUNNING = 1; private static final int STATE_FINISHED = 2; private static final int STATE_ABORTING = 4; private static final int STATE_ABORTED = 8; private static final int STATE_CANCELLING = 16; private static final int STATE_CANCELLED = 32; private Throwable fException; private boolean isFinished(int state) { return (state & (STATE_FINISHED | STATE_CANCELLED | STATE_ABORTED)) != 0; } @Override protected int tryAcquireShared(int ignore) { return doIsDone()? 1 : -1; } @Override protected boolean tryReleaseShared(int ignore) { return true; } boolean doIsCancelled() { int state = getState(); return (state & (STATE_CANCELLING | STATE_CANCELLED)) != 0; } boolean doIsDone() { return isFinished(getState()); } void doGet() throws InterruptedException, ExecutionException { acquireSharedInterruptibly(0); if (getState() == STATE_CANCELLED) throw new CancellationException(); if (fException != null) throw new ExecutionException(fException); } void doGet(long nanosTimeout) throws InterruptedException, ExecutionException, TimeoutException { if (!tryAcquireSharedNanos(0, nanosTimeout)) throw new TimeoutException(); if (getState() == STATE_CANCELLED) throw new CancellationException(); if (fException != null) throw new ExecutionException(fException); } void doAbort(Throwable t) { while(true) { int s = getState(); if (isFinished(s)) return; if (compareAndSetState(s, STATE_ABORTING)) break; } fException = t; } boolean doCancel() { while(true) { int s = getState(); if (isFinished(s)) return false; if (s == STATE_ABORTING) return false; if (compareAndSetState(s, STATE_CANCELLING)) break; } return true; } void doFinish() { while(true) { int s = getState(); if (isFinished(s)) return; if (s == STATE_ABORTING) { if (compareAndSetState(s, STATE_ABORTED)) break; } else if (s == STATE_CANCELLING) { if (compareAndSetState(s, STATE_CANCELLED)) break; } else { if (compareAndSetState(s, STATE_FINISHED)) break; } } releaseShared(0); } boolean doRun() { return compareAndSetState(0, STATE_RUNNING); } } }