/******************************************************************************* * Copyright (c) 2006, 2015 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 * Eugene Ostroukhov (NVIDIA) - new done(IStatus) method * Alvaro Sanchez-Leon (Ericsson) - Trace unsuccessful DSF Request Monitor at done() (Bug 453618) *******************************************************************************/ package org.eclipse.cdt.dsf.concurrent; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; import org.eclipse.cdt.dsf.internal.DsfPlugin; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Status; /** * Used to monitor the result of an asynchronous request. Because of the * asynchronous nature of DSF code, a very large number of methods needs to * signal the result of an operation through a call-back. This class is the base * class for such call backs. * <p> * The intended use of this class, is that a client who is calling an asynchronous * method, will sub-class RequestMonitor, and implement the method {@link #handleCompleted()}, * or any of the other <code>handle...</code> methods, in order to interpret the * results of the request. The object implementing the asynchronous method is required * to call the {@link #done()} method on the request monitor object that it received * as an argument. * </p> * <p> * The severity of the {@link IStatus> returned by #getStatus() can be used to * determine the success or failure of the asynchronous operation. By convention * the error codes returned by asynchronous method should be interpreted as follows: * <ul> * <li>OK and INFO - Result is a success. In DataRequestMonitor, getData() should * return a value.</li> * <li>WARNING - Acceptable error condition (getData() may return null). Where for * example user tried to retrieve variable data, but the program resumed in the * mean time and an event will be generated shortly which will clear the variables * view.</li> * <li>ERROR - An error condition that should probably be reported to the user.</li> * <li>CANCEL - The request was canceled, and the asynchronous method was not * completed.</li> * </ul> * </p> * <p> * The RequestMonitor constructor accepts an optional "parent" request monitor. If a * parent monitor is specified, it will automatically be invoked by this monitor when * the request is completed. The parent option is useful when implementing a method * which is asynchronous (and accepts a request monitor as an argument) and which itself * calls another asynchronous method to complete its operation. For example, in the * request monitor implementation below, the implementation only needs to override * <code>handleSuccess()</code>, because the base implementation will handle notifying the * parent <code>rm</code> in case the <code>getIngredients()</code> call fails. * <pre> * public void createCupCakes(final DataRequestMonitor<CupCake[]> rm) { * getIngredients(new DataRequestMonitor<Ingredients>(fExecutor, rm) { * public void handleSuccess() { * rm.setData( new CupCake(getData().getFlour(), getData().getSugar(), * getData().getBakingPowder())); * rm.done(); * } * }); * } * </pre> * </p> * * @since 1.0 */ @ThreadSafe public class RequestMonitor extends DsfExecutable { /** * Interface used by RequestMonitor to notify when a given request monitor * is canceled. * * @see RequestMonitor */ public static interface ICanceledListener { /** * Called when the given request monitor is canceled. */ public void requestCanceled(RequestMonitor rm); } /** * The executor that will be used in order to invoke the handler of the results * of the request. */ private final Executor fExecutor; /** * The request monitor which was used to call into the method that created this * monitor. */ private final RequestMonitor fParentRequestMonitor; private List<ICanceledListener> fCancelListeners; /** * Status */ private IStatus fStatus = Status.OK_STATUS; private boolean fCanceled = false; private boolean fDone = false; private final ICanceledListener fCanceledListener; /** * This field is never read by any code; its purpose is strictly to assist * developers debug DPF code. Developer can select this field in the * Variables view and see a monitor backtrace in the details pane. See * {@link DsfExecutable#DEBUG_MONITORS}. * * <p> * This field is set only when tracing is enabled. */ private String fMonitorBacktrace; /** * Constructor with an optional parent monitor. * * @param executor * This executor will be used to invoke the runnable that will * allow processing the completion code of this request monitor. * I.e., the runnable will call {@link #handleCompleted()}. * @param parentRequestMonitor * An optional parent request monitor. By default, our completion * handlers invoke the parent monitor's <code>done</code> method, * thus allowing monitors to be daisy chained. If this request is * unsuccessful, its status is set into the parent monitor. * Parameter may be null. */ public RequestMonitor(Executor executor, RequestMonitor parentRequestMonitor) { fExecutor = executor; fParentRequestMonitor = parentRequestMonitor; // If the parent rm is not null, add ourselves as a listener so that // this request monitor will automatically be canceled when the parent // is canceled. if (fParentRequestMonitor != null) { fCanceledListener = new ICanceledListener() { @Override public void requestCanceled(RequestMonitor rm) { cancel(); } }; fParentRequestMonitor.addCancelListener(fCanceledListener); } else { fCanceledListener = null; } if (DEBUG_MONITORS) { createMonitorBacktrace(); } } /** * Sets the status of the result of the request. If status is OK, this * method does not need to be called. * * @see #done(IStatus) */ public synchronized void setStatus(IStatus status) { assert isCanceled() || status.getSeverity() != IStatus.CANCEL; fStatus = status; } /** Returns the status of the completed method. */ public synchronized IStatus getStatus() { if (isCanceled()) { return Status.CANCEL_STATUS; } return fStatus; } /** * Sets this request monitor as canceled and calls the cancel listeners if * any. * <p> * Note: Calling cancel() does not automatically complete the * RequestMonitor. The asynchronous call still has to call done(). * </p> * <p> * Note: logically a request should only be canceled by the client that * issued the request in the first place. After a request is canceled, the * method that is fulfilling the request may call * {@link #setStatus(IStatus)} with severity of <code>IStatus.CANCEL</code> * to indicate that it recognized that the given request was canceled and it * did not perform the given operation. * </p> * <p> * Canceling a monitor effectively cancels all descendant monitors, by * virtue of the default implementation of {@link #isCanceled()}, which * checks not only its own state but that of its parent. However, only the * cancel listeners of the monitor directly canceled will be called. * </p> */ public void cancel() { ICanceledListener[] listeners = null; synchronized (this) { // Check to make sure the request monitor wasn't previously canceled. if (!fCanceled) { fCanceled = true; if (fCancelListeners != null) { listeners = fCancelListeners.toArray(new ICanceledListener[fCancelListeners.size()]); } } } // Call the listeners outside of a synchronized section to reduce the // risk of deadlocks. if (listeners != null) { for (ICanceledListener listener : listeners) { listener.requestCanceled(this); } } } /** * Returns whether the request was canceled. Even if the request is * canceled by the client, the implementor handling the request should * still call {@link #done()} in order to complete handling * of the request monitor. * * <p> * A request monitor is considered canceled if either it or its parent was canceled. * </p> */ public boolean isCanceled() { boolean canceled = false; // Avoid holding onto this lock while calling parent RM, which may // acquire other locks (bug 329488). synchronized(this) { canceled = fCanceled; } return canceled || (fParentRequestMonitor != null && fParentRequestMonitor.isCanceled()); } /** * Adds the given listener to list of listeners that are notified when this * request monitor is directly canceled. */ public synchronized void addCancelListener(ICanceledListener listener) { if (fCancelListeners == null) { fCancelListeners = new ArrayList<ICanceledListener>(1); } fCancelListeners.add(listener); } /** * Removes the given listener from the list of listeners that are notified * when this request monitor is directly canceled. */ public synchronized void removeCancelListener(ICanceledListener listener) { if (fCancelListeners != null) { fCancelListeners.remove(listener); } } /** * Marks this request as completed. Once this method is called, the * monitor submits a runnable to the DSF Executor to call the * <code>handle...</code> methods. * <p> * Note: Only one <code>done</code> method should be called and only once, * for every request issued. Even if the request was canceled. * </p> */ public synchronized void done() { setSubmitted(); if (fDone) { throw new IllegalStateException("RequestMonitor: " + this + ", done() method called more than once"); //$NON-NLS-1$//$NON-NLS-2$ } fDone = true; // This RequestMonitor is done, it can no longer be canceled. // We must clear the list of cancelListeners because it causes a // circular reference between parent and child requestMonitor, which // causes a memory leak. fCancelListeners = null; if (fParentRequestMonitor != null) { fParentRequestMonitor.removeCancelListener(fCanceledListener); } if (DEBUG_MONITORS && !isSuccess()) { IStatus status = getStatus(); try { throw new Exception(); } catch (Exception e) { DsfPlugin.getDefault().getLog().log(new Status(status.getSeverity(), DsfPlugin.PLUGIN_ID, status.getCode(), status.getMessage() + "\n\nMonitor Back Trace:\n" + fMonitorBacktrace.toString(), e)); //$NON-NLS-1$ } } try { fExecutor.execute(new DsfRunnable() { @Override public void run() { RequestMonitor.this.handleCompleted(); } @Override public String toString() { return "Completed: " + RequestMonitor.this.toString(); //$NON-NLS-1$ } }); } catch (RejectedExecutionException e) { handleRejectedExecutionException(); } } /** * Sets status and marks request monitor as completed. * * <p> * Note: Only one <code>done</code> method should be called and only once, * for every request issued. Even if the request was canceled. * </p> * * @param status Request processing status * @see #done() * @see #setStatus(IStatus) * @since 2.3 */ public synchronized void done(IStatus status) { setStatus(status); done(); } @Override public String toString() { return "RequestMonitor (" + super.toString() + "): " + getStatus().toString(); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Checks whether the given request monitor completed with success or * failure result. If the request monitor was canceled it is considered a * failure, regardless of the status. If the status has a severity higher * than INFO (i.e., WARNING, ERROR or CANCEL), it is considered a failure. */ public boolean isSuccess() { return !isCanceled() && getStatus().getSeverity() <= IStatus.INFO; } /** * First tier handler for the completion of the request. By default, the * {@link #done()} method drives this method on the executor specified at * construction time. By default, this handler merely calls a more * specialized handler, which in turn may call an even more specialized * handler, and so on, thus giving a subclass the ability to * compartmentalize its completion logic by overriding specific handlers. * All handlers are named <code>handleXxxxx</code>. More specifically, the * base implementation calls {@link #handleSuccess()} if the request * succeeded, and calls {@link #handleFailure()} otherwise. <br> * * The complete hierarchy of handlers is as follows: <br> * <pre> * + handleCompleted * - handleSuccess * + handleFailure * - handleCancel * + handleErrororWarning * - handleError * - handleWarning * </pre> * * <p> * Note: Sub-classes may override this method. */ @ConfinedToDsfExecutor("fExecutor") protected void handleCompleted() { if (isSuccess()) { handleSuccess(); } else { handleFailure(); } } /** * Default handler for a successful the completion of a request. If this * monitor has a parent monitor that was configured by the constructor, that * parent monitor is notified. Otherwise this method does nothing. * {@link #handleFailure()} or cancel otherwise. * <br> * Note: Sub-classes may override this method. */ @ConfinedToDsfExecutor("fExecutor") protected void handleSuccess() { if (fParentRequestMonitor != null) { fParentRequestMonitor.done(); } } /** * The default implementation of a cancellation or an error result of a * request. The implementation delegates to {@link #handleCancel()} and * {@link #handleErrorOrWarning()} as needed. * <br> * Note: Sub-classes may override this method. */ @ConfinedToDsfExecutor("fExecutor") protected void handleFailure() { assert !isSuccess(); if (isCanceled()) { handleCancel(); } else { if (getStatus().getSeverity() == IStatus.CANCEL) { DsfPlugin.getDefault().getLog().log(new Status( IStatus.ERROR, DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Request monitor: '" + this + "' resulted in a cancel status: " + getStatus() + ", even though the request is not set to cancel.", null)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } handleErrorOrWarning(); } } /** * The default implementation of an error or warning result of a request. * The implementation delegates to {@link #handleError()} and * {@link #handleWarning()} as needed. * <br> * Note: Sub-classes may override this method. */ @ConfinedToDsfExecutor("fExecutor") protected void handleErrorOrWarning() { if (getStatus().getSeverity() == IStatus.ERROR) { handleError(); } else { handleWarning(); } } /** * The default implementation of an error result of a request. If this * monitor has a parent monitor that was configured by the constructor, that * parent monitor is configured with a new status containing this error. * Otherwise the error is logged. * <br> * Note: Sub-classes may override this method. */ @ConfinedToDsfExecutor("fExecutor") protected void handleError() { if (fParentRequestMonitor != null) { fParentRequestMonitor.setStatus(getStatus()); fParentRequestMonitor.done(); } else { MultiStatus logStatus = new MultiStatus(DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Request for monitor: '" + toString() + "' resulted in an error.", null); //$NON-NLS-1$ //$NON-NLS-2$ logStatus.merge(getStatus()); DsfPlugin.getDefault().getLog().log(logStatus); } } /** * The default implementation of an error result of a request. If this * monitor has a parent monitor that was configured by the constructor, that * parent monitor is configured with a new status containing this warning. * Otherwise the warning is logged. * <br> * Note: Sub-classes may override this method. */ @ConfinedToDsfExecutor("fExecutor") protected void handleWarning() { if (fParentRequestMonitor != null) { fParentRequestMonitor.setStatus(getStatus()); fParentRequestMonitor.done(); } } /** * Default completion handler for a canceled request. If this monitor was * constructed with a parent monitor, the status is propagated up to it. * Otherwise this method does nothing. <br> * Note: Sub-classes may override this method. */ @ConfinedToDsfExecutor("fExecutor") protected void handleCancel() { if (fParentRequestMonitor != null) { if (getStatus().getSeverity() == IStatus.CANCEL && !fParentRequestMonitor.isCanceled()) { fParentRequestMonitor.setStatus(new Status( IStatus.ERROR, DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Sub-request " + toString() + " was canceled and not handled.'", null)); //$NON-NLS-1$ //$NON-NLS-2$ } else { fParentRequestMonitor.setStatus(getStatus()); } fParentRequestMonitor.done(); } } /** * Default handler for when the executor supplied in the constructor * rejects the runnable that is submitted invoke this request monitor. * This usually happens only when the executor is shutting down. * <p> * The default handler creates a new error status for the rejected * execution and propagates it to the client or logs it. */ protected void handleRejectedExecutionException() { IStatus rejectedStatus = new Status(IStatus.ERROR, DsfPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Request for monitor: '" + toString() + "' resulted in a rejected execution exception.", null); //$NON-NLS-1$ //$NON-NLS-2$ if (!getStatus().isOK()) { DsfMultiStatus multiStatus = new DsfMultiStatus(DsfPlugin.PLUGIN_ID, 0, "Composite status", null); //$NON-NLS-1$ multiStatus.merge(getStatus()); multiStatus.merge(rejectedStatus); rejectedStatus = multiStatus; } if (fParentRequestMonitor != null) { fParentRequestMonitor.setStatus(rejectedStatus); fParentRequestMonitor.done(); } else { DsfPlugin.getDefault().getLog().log(rejectedStatus); } } /** * Instrument this object with a backtrace of the monitors this instance is * chained to. See {@link DsfExecutable#DEBUG_MONITORS} */ private void createMonitorBacktrace() { StringBuilder str = new StringBuilder(); for (RequestMonitor nextrm = this; nextrm != null; nextrm = nextrm.fParentRequestMonitor) { final StackTraceElement[] stackTraceElems = (nextrm.fCreatedAt != null) ? nextrm.fCreatedAt.fStackTraceElements : null; if (stackTraceElems != null && stackTraceElems.length > 0) { str.append(stackTraceElems[0] + "\n"); //$NON-NLS-1$ } else { str.append("<unknown>\n"); //$NON-NLS-1$ } } fMonitorBacktrace = str.toString(); } }