All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.util.IteratingCallback Maven / Gradle / Ivy

//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.util;

import java.io.IOException;
import java.util.Objects;
import java.util.function.Consumer;

import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This specialized callback implements a pattern that allows
 * a large asynchronous task to be broken into smaller
 * asynchronous sub-tasks using iteration rather than recursion.
 * 

* A typical example is the write of a large content to a socket, * divided in chunks. Chunk C1 is written by thread T1, which * also invokes the callback, which writes chunk C2, which invokes * the callback again, which writes chunk C3, and so forth. *

* The problem with the example above is that if the callback thread * is the same that performs the I/O operation, then the process * is recursive and may result in a stack overflow. * To avoid the stack overflow, a thread dispatch must be performed, * causing context switching and cache misses, affecting performance. *

* To avoid this issue, this callback atomically records whether * the callback for an asynchronous sub-task has been called * during the processing of the asynchronous sub-task, and if so * then the processing of the large asynchronous task iterates * rather than recursing. *

* Subclasses must implement method {@link #process()} where the * asynchronous sub-task is initiated and a suitable {@link Action} * is returned to this callback to indicate the overall progress of * the large asynchronous task. * This callback is passed to the asynchronous sub-task, and a call * to {@link #succeeded()} on this callback represents the successful * completion of the asynchronous sub-task, while a call to * {@link #failed(Throwable)} on this callback represents the * completion with a failure of the large asynchronous task. */ public abstract class IteratingCallback implements Callback { private static final Logger LOG = LoggerFactory.getLogger(IteratingCallback.class); /** * The internal states of this callback. */ enum State { /** * This callback is idle, ready to iterate. */ IDLE, /** * This callback is just about to call {@link #process()}, * or within it, or just exited from it, either normally * or by throwing. Further actions are waiting for the * {@link #process()} method to return. */ PROCESSING, /** * The asynchronous sub-task was completed either with * a call to {@link #succeeded()} or {@link #failed(Throwable)}, whilst in * {@link #PROCESSING} state. Further actions are waiting for the * {@link #process()} method to return. */ PROCESSING_CALLED, /** * Method {@link #process()} returned {@link Action#SCHEDULED} * and this callback is waiting for the asynchronous sub-task * to complete via a callback to {@link #succeeded()} or {@link #failed(Throwable)} */ PENDING, /** * This callback is complete. */ COMPLETE, /** * Complete and can't be reset. */ CLOSED } /** * The indication of the overall progress of the iteration * that implementations of {@link #process()} must return. */ protected enum Action { /** * Indicates that {@link #process()} has no more work to do, * but the iteration is not completed yet, probably waiting * for additional events to trigger more work. */ IDLE, /** * Indicates that {@link #process()} has initiated an asynchronous * sub-task, where the execution has started but the callback * that signals the completion of the asynchronous sub-task * may have not yet been invoked. */ SCHEDULED, /** * Indicates that {@link #process()} has completed the whole * iteration successfully. */ SUCCEEDED } private final AutoLock _lock = new AutoLock(); private final Runnable _onSuccess = this::onSuccess; private final Runnable _processing = this::processing; private final Consumer _onCompleted = this::onCompleted; private State _state; private Throwable _failure; private boolean _reprocess; private boolean _aborted; protected IteratingCallback() { _state = State.IDLE; } protected IteratingCallback(boolean needReset) { _state = needReset ? State.COMPLETE : State.IDLE; } /** * Method called by {@link #iterate()} to process the asynchronous sub-task. *

* Implementations must initiate the asynchronous execution of the sub-task * (if any) and return an appropriate action: *

    *
  • {@link Action#IDLE} when no sub tasks are available for execution * but the overall job is not completed yet
  • *
  • {@link Action#SCHEDULED} when the sub task asynchronous execution * has been started
  • *
  • {@link Action#SUCCEEDED} when the overall job is completed
  • *
* * @return the appropriate Action * @throws Throwable if the sub-task processing throws */ protected abstract Action process() throws Throwable; /** * Invoked when one task has completed successfully, either by the * caller thread or by the processing thread. This invocation is * always serialized w.r.t the execution of {@link #process()}. *

* This method is not invoked when a call to {@link #abort(Throwable)} * is made before the {@link #succeeded()} callback happens. */ protected void onSuccess() { } /** * Invoked when the overall task has been {@link #abort(Throwable) aborted} or {@link #failed(Throwable) failed}. *

* Calls to this method are serialized with respect to {@link #onAborted(Throwable)}, {@link #process()}, * {@link #onCompleteFailure(Throwable)} and {@link #onCompleted(Throwable)}. *

* Because {@code onFailure} can be called due to an {@link #abort(Throwable)} or {@link #close()} operation, it is * possible that any resources passed to a {@link Action#SCHEDULED} operation may still be in use, and thus should not * be recycled by this call. For example any buffers passed to a write operation should not be returned to a buffer * pool by implementations of {@code onFailure}. Such resources may be discarded here, or safely recycled in a * subsequent call to {@link #onCompleted(Throwable)} or {@link #onCompleteFailure(Throwable)}, when * the {@link Action#SCHEDULED} operation has completed. * @param cause The cause of the failure or abort * @see #onCompleted(Throwable) * @see #onCompleteFailure(Throwable) */ protected void onFailure(Throwable cause) { } /** * Invoked when the overall task has completed successfully, specifically after any {@link Action#SCHEDULED} operations * have {@link Callback#succeeded()} and {@link #process()} has returned {@link Action#SUCCEEDED}. *

* Calls to this method are serialized with respect to {@link #process()}, {@link #onAborted(Throwable)} * and {@link #onCompleted(Throwable)}. * If this method is called, then {@link #onCompleteFailure(Throwable)} ()} will never be called. * * @see #onCompleteFailure(Throwable) */ protected void onCompleteSuccess() { } /** * Invoked when the overall task has completed with a failure. *

* Calls to this method are serialized with respect to {@link #process()}, {@link #onAborted(Throwable)} * and {@link #onCompleted(Throwable)}. * If this method is called, then {@link #onCompleteSuccess()} will never be called. * * @param cause the throwable to indicate cause of failure * @see #onCompleteSuccess() */ protected void onCompleteFailure(Throwable cause) { } /** * Invoked when the overall task has been aborted. *

* Calls to this method are serialized with respect to {@link #process()}, {@link #onCompleteFailure(Throwable)} * and {@link #onCompleted(Throwable)}. * If this method is called, then {@link #onCompleteSuccess()} will never be called. *

* The default implementation of this method calls {@link #failed(Throwable)}. Overridden implementations of * this method SHOULD NOT call {@code super.onAborted(Throwable)}. *

* Because {@code onAborted} can be called due to an {@link #abort(Throwable)} or {@link #close()} operation, it is * possible that any resources passed to a {@link Action#SCHEDULED} operation may still be in use, and thus should not * be recycled by this call. For example any buffers passed to a write operation should not be returned to a buffer * pool by implementations of {@code onFailure}. Such resources may be discarded here, or safely recycled in a * subsequent call to {@link #onCompleted(Throwable)} or {@link #onCompleteFailure(Throwable)}, when * the {@link Action#SCHEDULED} operation has completed. * @param cause The cause of the abort * @see #onCompleted(Throwable) * @see #onCompleteFailure(Throwable) */ protected void onAborted(Throwable cause) { } /** * Invoked when the overall task has completed. *

* Calls to this method are serialized with respect to {@link #process()} and {@link #onAborted(Throwable)}. * The default implementation of this method will call either {@link #onCompleteSuccess()} or {@link #onCompleteFailure(Throwable)} * thus implementations of this method should always call {@code super.onCompleted(Throwable)}. * * @param causeOrNull the cause of any {@link #abort(Throwable) abort} or {@link #failed(Throwable) failure}, * else {@code null} for {@link #succeeded() success}. */ protected void onCompleted(Throwable causeOrNull) { if (causeOrNull == null) onCompleteSuccess(); else onCompleteFailure(causeOrNull); } private void doOnSuccessProcessing() { ExceptionUtil.callAndThen(_onSuccess, _processing); } private void doCompleteSuccess() { onCompleted(null); } private void doOnCompleted(Throwable cause) { ExceptionUtil.call(cause, _onCompleted); } private void doOnFailureOnCompleted(Throwable cause) { ExceptionUtil.callAndThen(cause, this::onFailure, _onCompleted); } private void doOnAbortedOnFailure(Throwable cause) { ExceptionUtil.callAndThen(cause, this::onAborted, this::onFailure); } private void doOnAbortedOnFailureOnCompleted(Throwable cause) { ExceptionUtil.callAndThen(cause, this::doOnAbortedOnFailure, _onCompleted); } private void doOnAbortedOnFailureIfNotPendingDoCompleted(Throwable cause) { ExceptionUtil.callAndThen(cause, this::doOnAbortedOnFailure, this::ifNotPendingDoCompleted); } private void ifNotPendingDoCompleted() { Throwable completeFailure = null; try (AutoLock ignored = _lock.lock()) { _failure = _failure.getCause(); if (Objects.requireNonNull(_state) != State.PENDING) { // the callback completed, one way or another, so it is up to us to do the completion completeFailure = _failure; } } if (completeFailure != null) doOnCompleted(completeFailure); } /** * This method must be invoked by applications to start the processing * of asynchronous sub-tasks. *

* It can be called at any time by any thread, and its contract is that * when called, then the {@link #process()} method will be called during * or soon after, either by the calling thread or by another thread, but * in either case by one thread only. */ public void iterate() { boolean process = false; try (AutoLock ignored = _lock.lock()) { switch (_state) { case IDLE: _state = State.PROCESSING; process = true; break; case PROCESSING: case PROCESSING_CALLED: _reprocess = true; break; default: break; } } if (process) processing(); } private void processing() { // This should only ever be called when in processing state, however a failed or close call // may happen concurrently, so state is not assumed. boolean completeSuccess = false; Throwable onAbortedOnFailureOnCompleted = null; Throwable onFailureOnCompleted = null; Throwable onAbortedOnFailureIfNotPendingDoCompleted = null; // While we are processing processing: while (true) { // Call process to get the action that we have to take. Action action; try { action = process(); } catch (Throwable x) { action = null; failed(x); // Fall through to possibly invoke onCompleteFailure(). } boolean callOnSuccess = false; // acted on the action we have just received try (AutoLock ignored = _lock.lock()) { if (LOG.isDebugEnabled()) LOG.debug("processing {} {}", action, this); switch (_state) { case PROCESSING: { if (action == null) break processing; switch (action) { case IDLE: { if (_aborted) { _state = _failure instanceof ClosedException ? State.CLOSED : State.COMPLETE; onAbortedOnFailureOnCompleted = _failure; break processing; } // Has iterate been called while we were processing? if (_reprocess) { // yes, so skip idle and keep processing _reprocess = false; continue; } // No, so we can go idle _state = State.IDLE; break processing; } case SCHEDULED: { // we won the race against the callback, so the callback has to process and we can break processing _state = State.PENDING; _reprocess = false; if (_aborted) { onAbortedOnFailureIfNotPendingDoCompleted = _failure; _failure = new AbortingException(onAbortedOnFailureIfNotPendingDoCompleted); } break processing; } case SUCCEEDED: { // we lost the race against the callback, _reprocess = false; if (_aborted) { _state = _failure instanceof ClosedException ? State.CLOSED : State.COMPLETE; onAbortedOnFailureOnCompleted = _failure; } else { _state = State.COMPLETE; completeSuccess = true; } break processing; } default: { break; } } throw new IllegalStateException(String.format("%s[action=%s]", this, action)); } case PROCESSING_CALLED: { if (action != Action.SCHEDULED && action != null) { _state = State.CLOSED; onAbortedOnFailureOnCompleted = new IllegalStateException("Action not scheduled"); if (_failure == null) { _failure = onAbortedOnFailureOnCompleted; } else { ExceptionUtil.addSuppressedIfNotAssociated(_failure, onAbortedOnFailureIfNotPendingDoCompleted); onAbortedOnFailureOnCompleted = _failure; } break processing; } if (_failure != null) { if (_aborted) onAbortedOnFailureOnCompleted = _failure; else onFailureOnCompleted = _failure; _state = _failure instanceof ClosedException ? State.CLOSED : State.COMPLETE; break processing; } callOnSuccess = true; _state = State.PROCESSING; _reprocess = false; break; } default: throw new IllegalStateException(String.format("%s[action=%s]", this, action)); } } finally { if (callOnSuccess) onSuccess(); } } if (onAbortedOnFailureOnCompleted != null) doOnAbortedOnFailureOnCompleted(onAbortedOnFailureOnCompleted); else if (completeSuccess) doCompleteSuccess(); else if (onFailureOnCompleted != null) doOnFailureOnCompleted(onFailureOnCompleted); else if (onAbortedOnFailureIfNotPendingDoCompleted != null) doOnAbortedOnFailureIfNotPendingDoCompleted(onAbortedOnFailureIfNotPendingDoCompleted); } /** * Method to invoke when the asynchronous sub-task succeeds. *

* For most purposes, this method should be considered {@code final} and should only be * overridden in extraordinary circumstances. * Subclasses that override this method must always call {@code super.succeeded()}. * Such overridden methods are not serialized with respect to {@link #process()}, {@link #onCompleteSuccess()}, * {@link #onCompleteFailure(Throwable)}, nor {@link #onAborted(Throwable)}. They should not act on nor change any * fields that may be used by those methods. * Eventually, {@link #onSuccess()} is * called, either by the caller thread or by the processing * thread. */ @Override public final void succeeded() { boolean onSuccessProcessing = false; Throwable onCompleted = null; try (AutoLock ignored = _lock.lock()) { if (LOG.isDebugEnabled()) LOG.debug("succeeded {}", this); switch (_state) { case PROCESSING: { // Another thread is processing, so we just tell it the state _state = State.PROCESSING_CALLED; break; } case PENDING: { if (_aborted) { if (_failure instanceof AbortingException) { // Another thread is still calling onAborted, so we will let it do the completion _state = _failure.getCause() instanceof ClosedException ? State.CLOSED : State.COMPLETE; } else { // The onAborted call is complete, so we must do the completion _state = _failure instanceof ClosedException ? State.CLOSED : State.COMPLETE; onCompleted = _failure; } } else { // No other thread is processing, so we will do the processing _state = State.PROCESSING; onSuccessProcessing = true; } break; } case COMPLETE, CLOSED: { // Too late return; } default: { throw new IllegalStateException(toString()); } } } if (onSuccessProcessing) { doOnSuccessProcessing(); } else if (onCompleted != null) { doOnCompleted(onCompleted); } } /** * Method to invoke when the asynchronous sub-task fails, * or to fail the overall asynchronous task and therefore * terminate the iteration. *

* Eventually, {@link #onCompleteFailure(Throwable)} is * called, either by the caller thread or by the processing * thread. *

* For most purposes, this method should be considered {@code final} and should only be * overridden in extraordinary circumstances. * Subclasses that override this method must always call {@code super.succeeded()}. * Such overridden methods are not serialized with respect to {@link #process()}, {@link #onCompleteSuccess()}, * {@link #onCompleteFailure(Throwable)}, nor {@link #onAborted(Throwable)}. They should not act on nor change any * fields that may be used by those methods. * @see #isFailed() */ @Override public final void failed(Throwable cause) { cause = Objects.requireNonNullElseGet(cause, IOException::new); Throwable onFailureOnCompleted = null; Throwable onCompleted = null; try (AutoLock ignored = _lock.lock()) { if (LOG.isDebugEnabled()) LOG.debug("failed {}", this, cause); switch (_state) { case PROCESSING: { // Another thread is processing, so we just tell it the state _state = State.PROCESSING_CALLED; if (_failure == null) _failure = cause; else ExceptionUtil.addSuppressedIfNotAssociated(_failure, cause); break; } case PENDING: { if (_aborted) { if (_failure instanceof AbortingException) { // Another thread is still calling onAborted, so we will let it do the completion ExceptionUtil.addSuppressedIfNotAssociated(_failure.getCause(), cause); _state = _failure.getCause() instanceof ClosedException ? State.CLOSED : State.COMPLETE; } else { // The onAborted call is complete, so we must do the completion ExceptionUtil.addSuppressedIfNotAssociated(_failure, cause); _state = _failure instanceof ClosedException ? State.CLOSED : State.COMPLETE; onCompleted = _failure; } } else { // No other thread is processing, so we will do the processing _state = State.COMPLETE; _failure = cause; onFailureOnCompleted = _failure; } break; } case COMPLETE, CLOSED: { // Too late ExceptionUtil.addSuppressedIfNotAssociated(_failure, cause); return; } default: { throw new IllegalStateException(toString()); } } } if (onFailureOnCompleted != null) doOnFailureOnCompleted(onFailureOnCompleted); else if (onCompleted != null) doOnCompleted(onCompleted); } /** * Method to invoke to forbid further invocations to {@link #iterate()} * and {@link #reset()}. *

* When this method is invoked during processing, it behaves like invoking * {@link #failed(Throwable)}. * * @see #isClosed() */ public final void close() { Throwable onAbortedOnFailureIfNotPendingDoCompleted = null; Throwable onAbortedOnFailureOnCompleted = null; try (AutoLock ignored = _lock.lock()) { if (LOG.isDebugEnabled()) LOG.debug("close {}", this); switch (_state) { case IDLE -> { // Nothing happening so we can abort and complete _state = State.CLOSED; _failure = new ClosedException(); onAbortedOnFailureOnCompleted = _failure; } case PROCESSING, PROCESSING_CALLED -> { // Another thread is processing, so we just tell it the state and let it handle it if (_aborted) { ExceptionUtil.addSuppressedIfNotAssociated(_failure, new ClosedException()); } else { _aborted = true; _failure = new ClosedException(); } } case PENDING -> { // We are waiting for the callback, so we can only call onAbort and then keep waiting onAbortedOnFailureIfNotPendingDoCompleted = new ClosedException(); _failure = new AbortingException(onAbortedOnFailureIfNotPendingDoCompleted); _aborted = true; } case COMPLETE -> { _state = State.CLOSED; } case CLOSED -> { // too late return; } } } if (onAbortedOnFailureIfNotPendingDoCompleted != null) doOnAbortedOnFailureIfNotPendingDoCompleted(onAbortedOnFailureIfNotPendingDoCompleted); else if (onAbortedOnFailureOnCompleted != null) doOnAbortedOnFailureOnCompleted(onAbortedOnFailureOnCompleted); } /** *

Method to invoke to stop further processing iterations.

*

This method causes {@link #onCompleteFailure(Throwable)} to * ultimately be invoked, either during this call or later after * any call to {@link #process()} has returned.

* * @param cause the cause of the abort * @return {@code true} if abort was called before the callback was complete. * @see #isAborted() */ public final boolean abort(Throwable cause) { cause = Objects.requireNonNullElseGet(cause, Throwable::new); boolean onAbort = false; boolean onAbortedOnFailureOnCompleted = false; try (AutoLock ignored = _lock.lock()) { if (LOG.isDebugEnabled()) LOG.debug("abort {}", this, cause); // Are we already aborted? if (_aborted) { ExceptionUtil.addSuppressedIfNotAssociated(_failure, cause); return false; } switch (_state) { case IDLE: { // Nothing happening so we can abort and complete _state = State.COMPLETE; _failure = cause; _aborted = true; onAbortedOnFailureOnCompleted = true; break; } case PROCESSING: { // Another thread is processing, so we just tell it the state and let it handle everything _failure = cause; _aborted = true; break; } case PROCESSING_CALLED: { // Another thread is processing, but we have already succeeded or failed. if (_failure == null) _failure = cause; else ExceptionUtil.addSuppressedIfNotAssociated(_failure, cause); _aborted = true; break; } case PENDING: { // We are waiting for the callback, so we can only call onAbort and then keep waiting onAbort = true; _failure = new AbortingException(cause); _aborted = true; break; } case COMPLETE, CLOSED: { // too late ExceptionUtil.addSuppressedIfNotAssociated(_failure, cause); return false; } } } if (onAbortedOnFailureOnCompleted) doOnAbortedOnFailureOnCompleted(cause); else if (onAbort) doOnAbortedOnFailureIfNotPendingDoCompleted(cause); return true; } boolean isPending() { try (AutoLock ignored = _lock.lock()) { return _state == State.PENDING; } } /** * @return whether this callback is idle, and {@link #iterate()} needs to be called */ boolean isIdle() { try (AutoLock ignored = _lock.lock()) { return _state == State.IDLE; } } /** * @return whether this callback has been {@link #close() closed} */ public boolean isClosed() { try (AutoLock ignored = _lock.lock()) { return _state == State.CLOSED || _failure instanceof ClosedException; } } /** * @return whether this callback has been {@link #failed(Throwable) failed} */ public boolean isFailed() { try (AutoLock ignored = _lock.lock()) { return _failure != null; } } /** * @return whether this callback and the overall asynchronous task has been succeeded * * @see #onCompleteSuccess() */ public boolean isSucceeded() { try (AutoLock ignored = _lock.lock()) { return _state == State.COMPLETE && _failure == null; } } /** * @return whether this callback has been {@link #abort(Throwable) aborted} */ public boolean isAborted() { try (AutoLock ignored = _lock.lock()) { return _aborted; } } /** * Resets this callback. *

* A callback can only be reset to the idle state from the * {@link #isSucceeded() succeeded} or {@link #isFailed() failed} states * or if it is already idle. * * @return true if the reset was successful */ public boolean reset() { try (AutoLock ignored = _lock.lock()) { switch (_state) { case IDLE: return true; case COMPLETE: _state = State.IDLE; _failure = null; _reprocess = false; return true; default: return false; } } } @Override public String toString() { try (AutoLock ignored = _lock.lock()) { return String.format("%s@%x[%s, %b, %s]", getClass().getSimpleName(), hashCode(), _state, _aborted, _failure); } } private static class ClosedException extends Exception { ClosedException() { super("Closed"); } ClosedException(Throwable suppressed) { this(); ExceptionUtil.addSuppressedIfNotAssociated(this, suppressed); } } private static class AbortingException extends Exception { AbortingException(Throwable cause) { super(cause.getMessage(), cause); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy