On Wed, Sep 30, 2026 at 11:06 AM <[email protected]> wrote:
>
> This is an automated email from the ASF dual-hosted git repository.
>
> markt-asf pushed a commit to branch main
> in repository https://gitbox.apache.org/repos/asf/tomcat.git
>
>
> The following commit(s) were added to refs/heads/main by this push:
>      new 85daf45655 Follow-up to 09c0caced9
> 85daf45655 is described below
>
> commit 85daf4565576e76367978ad8bb27a57d15a27fd4
> Author: Mark Thomas <[email protected]>
> AuthorDate: Wed Sep 30 10:05:46 2026 +0100
>
>     Follow-up to 09c0caced9
>
>     Changes
>     - STARTING or READ_WRITE_OP not possible after complete()/dispatch()
>     - Handle the same race for read

My test endpoint is a bit overeager for write, and no other issue was
found. So probably it's not quite needed for read in practice (but you
never know I guess).

Rémy

> ---
>  java/org/apache/coyote/AbstractProcessor.java  | 19 +++++++---
>  java/org/apache/coyote/AsyncStateMachine.java  | 50 
> ++++++++++----------------
>  java/org/apache/coyote/LocalStrings.properties |  1 +
>  java/org/apache/coyote/ajp/AjpProcessor.java   |  5 +--
>  4 files changed, 37 insertions(+), 38 deletions(-)
>
> diff --git a/java/org/apache/coyote/AbstractProcessor.java 
> b/java/org/apache/coyote/AbstractProcessor.java
> index c5667b93af..3b8b9cb73a 100644
> --- a/java/org/apache/coyote/AbstractProcessor.java
> +++ b/java/org/apache/coyote/AbstractProcessor.java
> @@ -228,7 +228,7 @@ public abstract class AbstractProcessor extends 
> AbstractProcessorLight implement
>      public final SocketState dispatch(SocketEvent status) throws IOException 
> {
>
>          if (status == SocketEvent.OPEN_WRITE && response.getWriteListener() 
> != null) {
> -            if (!asyncStateMachine.asyncOperationForWriteNotification()) {
> +            if (!asyncStateMachine.asyncOperation()) {
>                  // The notification raced with async completion on another
>                  // thread. Nothing to notify; returning LONG lets
>                  // asyncPostProcess() complete the cycle as usual.
> @@ -250,7 +250,16 @@ public abstract class AbstractProcessor extends 
> AbstractProcessorLight implement
>                  request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, ioe);
>              }
>          } else if (status == SocketEvent.OPEN_READ && 
> request.getReadListener() != null) {
> -            dispatchNonBlockingRead();
> +            if (!dispatchNonBlockingRead()) {
> +                // The notification raced with async completion on another
> +                // thread. Nothing to notify; returning LONG lets
> +                // asyncPostProcess() complete the cycle as usual.
> +                if (getLog().isTraceEnabled()) {
> +                    
> getLog().trace(sm.getString("abstractProcessor.lateReadNotification",
> +                            request.requestURI()));
> +                }
> +                return SocketState.LONG;
> +            }
>          } else if (status == SocketEvent.ERROR) {
>              // An I/O error occurred on a non-container thread. This 
> includes:
>              // - read/write timeouts fired by the Poller in NIO
> @@ -709,9 +718,11 @@ public abstract class AbstractProcessor extends 
> AbstractProcessorLight implement
>
>      /**
>       * Perform any necessary processing for a non-blocking read before 
> dispatching to the adapter.
> +     *
> +     * @return {@code true} if the read listener should be notified, 
> otherwise {@code false}
>       */
> -    protected void dispatchNonBlockingRead() {
> -        asyncStateMachine.asyncOperation();
> +    protected boolean dispatchNonBlockingRead() {
> +        return asyncStateMachine.asyncOperation();
>      }
>
>
> diff --git a/java/org/apache/coyote/AsyncStateMachine.java 
> b/java/org/apache/coyote/AsyncStateMachine.java
> index 129b7bf88f..858b19bcf3 100644
> --- a/java/org/apache/coyote/AsyncStateMachine.java
> +++ b/java/org/apache/coyote/AsyncStateMachine.java
> @@ -252,44 +252,30 @@ class AsyncStateMachine {
>          }
>      }
>
> -    synchronized void asyncOperation() {
> -        if (state == AsyncState.STARTED) {
> -            updateState(AsyncState.READ_WRITE_OP);
> -        } else {
> -            throw new IllegalStateException(
> -                    sm.getString("asyncStateMachine.invalidAsyncState", 
> "asyncOperation()", state));
> -        }
> -    }
> -
> -    /*
> -     * Entry point for transport generated OPEN_WRITE write-listener
> -     * notifications. Unlike asyncOperation(), which the container calls when
> -     * it is about to perform an application initiated non-blocking write, a
> -     * notification may legitimately race with the completion of the async
> -     * cycle: the transport may queue the event while the write listener is
> -     * still active and only deliver the dispatch after the application has
> -     * completed the response on another thread. Once the cycle is completing
> -     * there is nothing left to notify (the listener will not be called 
> again)
> -     * and the completion path in asyncPostProcess() takes care of flushing
> -     * buffered data and firing onComplete().
> +    /**
> +     * Updates the state machine after a "ready for read" or "ready for 
> write" notification has been received from the
> +     * network layer.
>       *
> -     * Returns true if the caller should notify the write listener, false if
> -     * the notification raced with completion and should be treated as
> -     * handled without notifying the listener.
> +     * @return {@code true} if the caller should notify the associated 
> listener, otherwise {@code false}
>       */
> -    synchronized boolean asyncOperationForWriteNotification() {
> -        // States in which the cycle is completing (or an application
> -        // initiated write operation is already in progress): 
> asyncPostProcess
> -        // () has a completion branch for each of these, so ignoring the
> -        // notification lets the completion proceed normally.
> -        if (state == AsyncState.READ_WRITE_OP || state == 
> AsyncState.STARTING ||
> -                state == AsyncState.MUST_COMPLETE || state == 
> AsyncState.COMPLETE_PENDING ||
> +    synchronized boolean asyncOperation() {
> +        if (state == AsyncState.STARTED) {
> +            updateState(AsyncState.READ_WRITE_OP);
> +            return true;
> +        } else if (state == AsyncState.MUST_COMPLETE || state == 
> AsyncState.COMPLETE_PENDING ||
>                  state == AsyncState.COMPLETING || state == 
> AsyncState.MUST_DISPATCH ||
>                  state == AsyncState.DISPATCH_PENDING || state == 
> AsyncState.DISPATCHING) {
> +            /*
> +             * It is possible that a read or write notification could race 
> with a completion or dispatch. In that
> +             * scenario if the complete/dispatch win the later call to this 
> method triggered by the read/write
> +             * notification will trigger an ISE. Therefore, if the state 
> machine has already processed a
> +             * complete/dispatch, ignore the read/write notification.
> +             */
>              return false;
> +        } else {
> +            throw new IllegalStateException(
> +                    sm.getString("asyncStateMachine.invalidAsyncState", 
> "asyncOperation()", state));
>          }
> -        asyncOperation();
> -        return true;
>      }
>
>      /*
> diff --git a/java/org/apache/coyote/LocalStrings.properties 
> b/java/org/apache/coyote/LocalStrings.properties
> index e806321ab3..385f9713fd 100644
> --- a/java/org/apache/coyote/LocalStrings.properties
> +++ b/java/org/apache/coyote/LocalStrings.properties
> @@ -30,6 +30,7 @@ abstractProcessor.fallToDebug=\n\
>  \ Note: further occurrences of request parsing errors will be logged at 
> DEBUG level.
>  abstractProcessor.hostInvalid=The host [{0}] is not valid
>  abstractProcessor.httpupgrade.notsupported=HTTP upgrade is not supported by 
> this protocol
> +abstractProcessor.lateReadNotification=Ignoring late read notification for 
> request [{0}] because the async cycle is completing
>  abstractProcessor.lateWriteNotification=Ignoring late write notification for 
> request [{0}] because the async cycle is completing
>  abstractProcessor.noExecute=Unable to transfer processing to a container 
> thread because this Processor is not currently associated with a SocketWrapper
>  abstractProcessor.setErrorState=Error state [{0}] reported while processing 
> request
> diff --git a/java/org/apache/coyote/ajp/AjpProcessor.java 
> b/java/org/apache/coyote/ajp/AjpProcessor.java
> index 5b88662683..ba5f56e0cd 100644
> --- a/java/org/apache/coyote/ajp/AjpProcessor.java
> +++ b/java/org/apache/coyote/ajp/AjpProcessor.java
> @@ -308,10 +308,11 @@ public class AjpProcessor extends AbstractProcessor {
>
>
>      @Override
> -    protected void dispatchNonBlockingRead() {
> +    protected boolean dispatchNonBlockingRead() {
>          if (available(true) > 0) {
> -            super.dispatchNonBlockingRead();
> +            return super.dispatchNonBlockingRead();
>          }
> +        return true;
>      }
>
>
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: [email protected]
> For additional commands, e-mail: [email protected]
>

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to