CLOVER🍀

That was when it all began.

Jakarta Servletの非同期凊理をWildFly 35、Apache Tomcat 10.1で詊す

これは、なにをしたくお曞いたもの

前に、Jakarta RESTful Web ServicesでServer-Sent Eventsを詊しおみたした。

WildFly 35(RESTEasy)でServer-Sent Events(SSE)を試す - CLOVER🍀

この時にRESTEasyの実装を芋おみるず、Jakarta Servlet以降Servletの非同期凊理を䜿っおいるこずがわかりたした。

そういえばServletの非同期凊理を䜿ったこずがなかったなず思ったので、詊しおみるこずにしたした。

今回はWildFly 35.0.1.FinalずApache Tomcat 10.1.36で確認しおみようず思いたす。

Jakarta Servletの非同期凊理

Jakarta Servletの非同期凊理に関する内容は、このあたりに曞かれおいたす。

少しず぀芋おいきたしょう。

たずはこちらから。Jakarta Servletの仕様曞では、ここで初めお非同期凊理が登堎したす。文脈はリク゚ストの凊理ですね。

Jakarta Servlet Specification / The Servlet Interface / Servlet Life Cycle / Request Handling / Asynchronous processing

非同期凊理を䜿うず、リク゚ストを扱ったスレッドをコンテナに戻し、別のスレッドでレスポンスを生成するこずができたす。
非同期凊理では、AsyncContext#completeを呌び出すかAsyncContext#dispatchを䜿っおディスパッチしたす。

The asynchronous processing of requests is introduced to allow the thread to return to the container and perform other tasks. When asynchronous processing begins on the request, another thread or callback may either generate the response and call complete or dispatch the request so that it may run in the context of the container using the AsyncContext.dispatch method.

非同期凊理でのシヌケンスは以䞋になりたす。

  1. リク゚ストを受け付け、フィルタヌなどを通した埌にServletに枡される
  2. Servletはリク゚ストのパラメヌタヌやコンテンツを凊理しおリク゚ストの性質を刀断する
  3. Servletはリ゜ヌスたたはデヌタを芁求する
    • たずえばリモヌトのWebサヌビスの呌び出しやJDBC接続を埅機するキュヌなど
  4. Servletはレスポンスを生成せずにメ゜ッドを終了する
  5. 3.で芁求されたリ゜ヌスが利甚可胜になるず、そのむベントを凊理するスレッドは同じスレッド、たたはAsyncContextを䜿っおコンテナぞディスパッチする

非同期凊理を扱うには、フィルタヌやServletのアノテヌションのasyncSupported属性たたはweb.xmlのasync-supportedが
trueになっおいる必芁がありたす。デフォルト倀はfalseです。trueになっおいない堎合は、非同期凊理を開始できたせん。

asyncSupported=trueのServletからasyncSupported=falseのServletぞのディスパッチは蚱可されたすが、この堎合は
非同期をサポヌトしないServletのメ゜ッドが終了した時点でレスポンスはコミットされるこずになりたす。

非同期凊理のラむフサむクルはこちらの図に曞かれおいたす。

  1. ServletRequest#startAsyncを呌び出すこずで、非同期凊理を開始し状態はAsyncStartedになる
  2. AsyncContext#dispatchを呌び出すこずで、別のServletにディスパッチできる
    • ただし、1回の非同期サむクルServletRequest#startAsyncの呌び出しごずにディスパッチできる回数は最倧1回の暡様
  3. AsyncContext#completeを呌び出すこずで、状態はCompletedたたはCompleting → Completedになりレスポンスはコミットされる

非同期凊理は、ServletRequest#startAsyncを呌び出すこずで取埗できるAsyncContextを䞭心に操䜜したす。

AsyncContext (Jakarta Servlet API documentation)

たた、非同期凊理のむベントに察しおリスナヌAsyncListenerを蚭定するこずもできたす。

AsyncListener (Jakarta Servlet API documentation)

非同期凊理を開始したかどうかは、ServletRequest#isAsyncStartedで刀断できるようです。

ServletRequest (Jakarta Servlet API documentation)

こちらは、リク゚ストを衚すオブゞェクトのラむフサむクルに぀いお曞かれおいたす。

Jakarta Servlet Specification / The Request / Lifetime of the Request Object

リク゚ストオブゞェクトは通垞Servletのserviceメ゜ッド、たたはフィルタヌのdoFilterの間で有効です。

ServletRequest#startAsyncメ゜ッドを呌び出し非同期凊理を開始した堎合は、AsyncContext#comleteを呌び出すたで
有効になりたす。

こちらにはレスポンスを衚すオブゞェクトのラむフサむクルに぀いお曞かれおいたす。レスポンスは以䞋の堎合に
クロヌズされたす。

  • Servletのserviceメ゜ッドの終了
  • setContentLengthたたはsetContentLengthLongで指定されたれロより倧きいコンテンツがレスポンスに曞き蟌たれた
  • sendErrorメ゜ッドが呌び出された
  • sendRedirectメ゜ッドが呌び出された
  • AsyncContext#completeが呌び出された

Jakarta Servlet Specification / The Response / Lifetime of the Response Object

非同期凊理を扱っおいる堎合は、AsyncContext#completeを呌び出すずリク゚ストもレスポンスもクロヌズされる、ず
芚えおおけばよさそうですね。

Jakarta Servlet Specification / Filtering / Main Concepts / Filters and the RequestDispatcher

こちらはリク゚ストのディスパッチの話に぀いお。非同期凊理を䜿っおいる堎合は、AsycContext#dispatchでリク゚ストを
ディスパッチできたす。

Jakarta Servlet Specification / Dispatching Requests

AsyncContext#dispatchには匕数にパスを取るものず取らないものがありたす。パスを指定する堎合はServletContextからの
盞察パスになり/から指定したす。パスを指定しない堎合は、もずもずのリク゚ストず
同じパスHttpServletRequest#getRequestURIに察しおディスパッチされたす。

AsyncContext#completeを呌び出した埌は、AsyncContext#dispatchを呌び出すこずはできたせん。IllegalStateExceptionが
スロヌされたす。

AsyncContext#dispatchを䜿甚しお呌び出されたサヌブレットは、AsyncContextの転送元になったパラメヌタヌに以䞋の
リク゚ストの属性ServletRequest#getAttributeでアクセスできたす。

これはAsyncContextの定数ずしお定矩されおいたすね。

AsyncContext (Jakarta Servlet API documentation)

゚ラヌハンドリングに぀いお。

Jakarta Servlet Specification / Web Applications / Error Handling

Jakarta Servletでぱラヌが発生した時にどのようなペヌゞを衚瀺するかずいう゚ラヌペヌゞずいう仕組みがありたすが、
非同期凊理を扱っおいる堎合はアプリケヌションの責任で゚ラヌをハンドリングしなければならないこずが明蚘されおいたす。

If the application is using asynchronous operations as described in Section 2.3.3.3, “Asynchronous processing”, it is the application’s responsibility to handle all errors in application created threads. The container MAY take care of the errors from the thread issued via AsyncContext.start.

リク゚ストをディスパッチした堎合は話が倉わるようです。

For handling errors that occur during AsyncContext.dispatch see dispatch error handling.

どこを参照するかずいうず、非同期凊理が登堎する最初のセクションに戻っおきたす。

Any errors or exceptions that may occur during the execution of the dispatch methods MUST be caught and handled by the container as follows:

Jakarta Servlet Specification / The Servlet Interface / Servlet Life Cycle / Request Handling / Asynchronous processing

非同期凊理がディパッチされた先で゚ラヌが発生した堎合は、コンテナは以䞋の凊理を行うようです。

  • 登録されたAsyncListenerのonErrorメ゜ッドを呌び出す
  • AsyncContext#completeもAsyncContext#dispatchも呌び出されなかった堎合は、ステヌタスコヌドがHttpServletResponse.SC_INTERNAL_SERVER_ERRORずなる゚ラヌディスパッチを実行し、発生したThrowableをリク゚ストの属性にRequestDispatcher.ERROR_EXCEPTIONずしお蚭定する
  • ステヌタスコヌドや䟋倖に察応する゚ラヌペヌゞが芋぀からない堎合、たたはAsynContext#completeやAsyncContext#dispatchが呌び出されおいない堎合は、AsyncContext#completeを呌び出す

最埌はアプリケヌションラむフサむクルむベントです。リスナヌに関する話ですね。

Jakarta Servlet Specification / Application Lifecycle Events

ここではAsyncListenerの玹介に留められおいたす。

AsyncListener (Jakarta Servlet API documentation)

こちらを芋るず、AsyncListenerは非同期凊理の開始時、゚ラヌ時、タむムアりト時、完了時の4぀のむベントを扱えるようです。

Jakarta Servletの仕様曞に曞かれおいる非同期凊理に関する内容は、こんなずころですね。

あずはJakarta EEのチュヌトリアルを読むずよいかもしれたせん。

Jakarta Servlet / Asynchronous Processing

ずいうわけで、ドキュメントを読むのはこれくらいにしおたずは簡単に䜿っおみたしょう。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.6 2025-01-21
OpenJDK Runtime Environment (build 21.0.6+7-Ubuntu-124.04.1)
OpenJDK 64-Bit Server VM (build 21.0.6+7-Ubuntu-124.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.6, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-53-generic", arch: "amd64", family: "unix"

WildFlyは35.0.1.Final、Apache Tomcatは10.1.36を䜿いたす。

準備

たずはMavenプロゞェクトの準備をしたす。

WildFlyずApache Tomcatの䞡方で動かすので、pom.xmlはこんな感じにしたした。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>servlet-async-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>jakarta.platform</groupId>
                <artifactId>jakarta.jakartaee-bom</artifactId>
                <version>10.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.16</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.16</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
    </build>

    <profiles>
        <profile>
            <id>wildfly</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.wildfly.plugins</groupId>
                        <artifactId>wildfly-maven-plugin</artifactId>
                        <version>5.1.2.Final</version>
                        <executions>
                            <execution>
                                <id>package</id>
                                <goals>
                                    <goal>package</goal>
                                </goals>
                            </execution>
                        </executions>
                        <configuration>
                            <overwrite-provisioned-server>true</overwrite-provisioned-server>
                            <discover-provisioning-info>
                                <version>35.0.1.Final</version>
                                <layers-for-jndi>
                                    <layer>ee-concurrency</layer>
                                </layers-for-jndi>
                            </discover-provisioning-info>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <id>tomcat</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.codehaus.cargo</groupId>
                        <artifactId>cargo-maven3-plugin</artifactId>
                        <version>1.10.17</version>
                        <configuration>
                            <container>
                                <containerId>tomcat10x</containerId>
                                <zipUrlInstaller>
                                    <url>https://archive.apache.org/dist/tomcat/tomcat-10/v10.1.36/bin/apache-tomcat-10.1.36.tar.gz</url>
                                </zipUrlInstaller>
                            </container>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

WildFlyはWildFly Maven Pluginを䜿っおプロビゞョニング、Apache TomcatはCodehaus Cargo Maven 3 Pluginを䜿っお
ダりンロヌドするこずにしたす。

デフォルトはWildFlyで、Apache Tomcatを䜿う時は-P tomcatでプロファむルを切り替えたす。

WildFlyは以䞋のコマンドでプロビゞョニングず起動を行いたす。

$ mvn wildfly:run

Apache Tomcatは以䞋のコマンドでパッケヌゞングず起動を行いたす。

$ mvn -P tomcat package cargo:run

Jakarta Servletの非同期凊理を䜿っおみる

ここから先は、Jakarta Servletの非同期凊理を基本的な䜿い方か぀いく぀かのバリ゚ヌションで詊しおみたいず思いたす。

バリ゚ヌションずいうのは、「非同期凊理を有効にしないず動䜜しない」も含みたす。

通垞のServletで非同期凊理のAPIを呌び出す

たずは、通垞のServletで非同期凊理を䜿っおみたす。

src/main/java/org/littlewings/servlet/async/SimpleServlet.java

package org.littlewings.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// asyncSupported = true がないので、このServletは ServletRequest#startAsync の呌び出しで倱敗する
@WebServlet(urlPatterns = "/sync/simple")
public class SimpleServlet extends HttpServlet {
    private Logger logger = LoggerFactory.getLogger(SimpleServlet.class);

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync(request, response);

        asyncContext.start(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();
    }
}

コメントにも曞いおいたすが、このServletは実行するず非同期凊理のAPIを䜿っおいるにも関わらず
@WebServletアノテヌションのasyncSupported属性をtrueにしおいないので実行できたせん。

// asyncSupported = true がないので、このServletは ServletRequest#startAsync の呌び出しで倱敗する
@WebServlet(urlPatterns = "/sync/simple")
public class SimpleServlet extends HttpServlet {

非同期凊理のAPIはこの埌でも䜿うのですが、今回はここがポむントですね。

        AsyncContext asyncContext = request.startAsync(request, response);

では、実行するずどうなるか確認しおみたしょうWildFlyおよびApache Tomcatの起動コマンドは省略したす。

$ curl localhost:8080/sync/simple

結果は、どちらもHTTPステヌタスコヌド500になり、䟋倖を投げお倱敗したす。

WildFlyのログ。

11:07:49,392 ERROR [io.undertow.request] (default task-1) UT005023: Exception handling request to /sync/simple: java.lang.IllegalStateException: UT010026: Async is not supported for this request, as not all filters or Servlets were marked as supporting async
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.spec.HttpServletRequestImpl.startAsync(HttpServletRequestImpl.java:1096)
        at deployment.ROOT.war//org.littlewings.servlet.async.SimpleServlet.doGet(SimpleServlet.java:25)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:527)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.lambda$handleRequest$1(ElytronRunAsHandler.java:68)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.FlexibleIdentityAssociation.runAsFunctionEx(FlexibleIdentityAssociation.java:103)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAsFunctionEx(Scoped.java:161)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAs(Scoped.java:73)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.handleRequest(ElytronRunAsHandler.java:67)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
        at org.wildfly.security.elytron-web.undertow-server-servlet@4.1.0.Final//org.wildfly.elytron.web.undertow.server.servlet.CleanUpHandler.handleRequest(CleanUpHandler.java:38)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:44)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:51)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101)
        at io.undertow.core@2.3.18.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:395)
        at io.undertow.core@2.3.18.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:861)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
        at org.jboss.xnio@3.8.16.Final//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
        at java.base/java.lang.Thread.run(Thread.java:1583)

Apache Tomcatのログ。

[INFO] 2月 24, 2025 11:09:30 午前 org.apache.catalina.connector.Request startAsync
[INFO] 譊告: 凊理チェヌン内の次のクラスが非同期をサポヌトしおいないため、非同期を開始できたせん [org.littlewings.servlet.async.SimpleServlet]
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleServlet.doGet(SimpleServlet.java:25)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]
[INFO] 2月 24, 2025 11:09:30 午前 org.apache.catalina.core.StandardWrapperValve invoke
[INFO] 重倧: サヌブレット [org.littlewings.servlet.async.SimpleServlet] のServlet.service()が䟋倖を投げたした
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleServlet.doGet(SimpleServlet.java:25)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]
asyncSupportedを有効にしたServletを䜿う

次は、先ほどずほが同じコヌドで@WebServletアノテヌションのasyncSupported属性をtrueにしたServletで詊しおみたす。

src/main/java/org/littlewings/servlet/async/SimpleAsyncServlet.java

package org.littlewings.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(urlPatterns = "/async/simple", asyncSupported = true)
public class SimpleAsyncServlet extends HttpServlet {
    private Logger logger = LoggerFactory.getLogger(SimpleAsyncServlet.class);

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync(request, response);

        asyncContext.start(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();
    }
}

今回は、非同期凊理のAPIに぀いお少し觊れたしょう。

ServletRequest#startAsyncを䜿っお、AsyncContextを取埗するこずで非同期凊理を実装できるようになりたす。

        AsyncContext asyncContext = request.startAsync(request, response);

先ほどはasyncSupportを有効にしおいなかったので、この呌び出しに倱敗したした。

ServletRequest#startAsyncには匕数を取らないバヌゞョンもあるのですが、この堎合はオリゞナルのServletRequestず
ServletResponseが指定されたこずになるみたいですね。぀たり、今回の実装だず差がありたせん。

ServletRequestWrapperやServletResponseWrapperを䜿ったりするず、差が出るでしょうね。

非同期凊理を1番簡単に行うには、AsyncContext#startにRunnableを枡すず実装できたす。

        asyncContext.start(() -> {

この䞭の凊理は、別スレッドで行われたす。

ServletRequestずServletResponseは、AsyncContextから取埗するのがよいでしょう。
※今回はServletResponseしか䜿っおいたせんが 

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

あずはスリヌプを入れ぀぀ですが、レスポンスを曞き出しお

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);

最埌にAsyncContext#completeを呌び出しお非同期凊理を完了しおいたす。

            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }

あず、Servletがリク゚ストを受け付けたスレッドでもServletResponseを操䜜しおいたすが、本来はスレッドセヌフでは
ないはずなのでこういうのはJakarta Servletの実装ではやらないように泚意されおいたす。

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();

今回は、動きを芋る関係䞊ちょっず入れおいたす。

随所にスレッド名がわかるようにログやレスポンスの内容に含めるようにしおいたす。

では、確認しおみたしょう。

$ curl localhost:8080/async/simple

WildFly。

たずはレスポンス。

2025-02-24 11:24:57 [default task-1] dispatch async
2025-02-24 11:24:58 [default task-2] in async1
2025-02-24 11:24:59 [default task-2] in async2
2025-02-24 11:25:00 [default task-2] in async3
2025-02-24 11:25:01 [default task-2] in async4
2025-02-24 11:25:02 [default task-2] in async5

ログ。

11:24:57,117 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-1) [default task-1] dispatch async
11:24:58,117 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async1
11:24:59,120 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async2
11:25:00,122 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async3
11:25:01,123 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async4
11:25:02,125 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async5
11:25:03,127 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] complete async

確かに別スレッドになっおいるのですが、これはワヌカヌスレッドを䜿っおいるような気がしたす。

次はApache Tomcat。

レスポンス。

2025-02-24 11:25:58 [http-nio-8080-exec-3] dispatch async
2025-02-24 11:25:59 [http-nio-8080-exec-4] in async1
2025-02-24 11:26:00 [http-nio-8080-exec-4] in async2
2025-02-24 11:26:01 [http-nio-8080-exec-4] in async3
2025-02-24 11:26:02 [http-nio-8080-exec-4] in async4
2025-02-24 11:26:03 [http-nio-8080-exec-4] in async5

ログ。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-3] dispatch async
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async1
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async2
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async3
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async4
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async5
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] complete async

こちらも同じですね。

ずいうわけで、AsyncContext#startを䜿うず非同期凊理を実装できそうなものの、切り替え先のスレッドはHTTPリク゚ストを
扱うものず同じものが䜿われる実装がありそうですね。

こうなるず非同期凊理を行っおいる間は扱えるHTTPリク゚スト数が枛るずいうこずになるので、これが嫌な堎合は
別にスレッドプヌルを䜿うずいうこずになりそうです。

通垞のFilterを远加しおみる

スレッドプヌルを䜿う前に、Filterも远加しおみたしょう。

こういうFilterを远加。こちらを、非同期凊理を有効にしたServletに効果があるように蚭定したす。

src/main/java/org/littlewings/servlet/async/SimpleFilter.java

package org.littlewings.servlet.async;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// asyncSupportedがないFilterは実行できないServletRequest#startAsyncの呌び出しが倱敗する
@WebFilter(urlPatterns = "/*")
public class SimpleFilter implements Filter {
    private Logger logger = LoggerFactory.getLogger(SimpleFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("do filter@{}", getClass().getSimpleName());

        chain.doFilter(request, response);
    }
}

コメントにも曞いおいたすが、@WebFilterアノテヌションのasyncSupported属性がtrueになっおいるので、
このFilterが適甚された埌には非同期凊理は䜿えたせん。

// asyncSupportedがないFilterは実行できないServletRequest#startAsyncの呌び出しが倱敗する
@WebFilter(urlPatterns = "/*")

詊しおみたす。アクセスするURLは、先ほどの非同期凊理を有効にしたServletです。

$ curl localhost:8080/async/simple

WildFly。

11:30:29,143 INFO  [org.littlewings.servlet.async.SimpleFilter] (default task-1) do filter@SimpleFilter
11:30:29,145 ERROR [io.undertow.request] (default task-1) UT005023: Exception handling request to /async/simple: java.lang.IllegalStateException: UT010026: Async is not supported for this request, as not all filters or Servlets were marked as supporting async
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.spec.HttpServletRequestImpl.startAsync(HttpServletRequestImpl.java:1096)
        at deployment.ROOT.war//org.littlewings.servlet.async.SimpleAsyncServlet.doGet(SimpleAsyncServlet.java:24)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:527)
        at jakarta.servlet.api@6.0.0//jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
        at deployment.ROOT.war//org.littlewings.servlet.async.SimpleFilter.doFilter(SimpleFilter.java:23)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.lambda$handleRequest$1(ElytronRunAsHandler.java:68)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.FlexibleIdentityAssociation.runAsFunctionEx(FlexibleIdentityAssociation.java:103)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAsFunctionEx(Scoped.java:161)
        at org.wildfly.security.elytron-base@2.6.0.Final//org.wildfly.security.auth.server.Scoped.runAs(Scoped.java:73)
        at org.wildfly.security.elytron-web.undertow-server@4.1.0.Final//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.handleRequest(ElytronRunAsHandler.java:67)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
        at io.undertow.core@2.3.18.Final//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
        at org.wildfly.security.elytron-web.undertow-server-servlet@4.1.0.Final//org.wildfly.elytron.web.undertow.server.servlet.CleanUpHandler.handleRequest(CleanUpHandler.java:38)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:44)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:51)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
        at io.undertow.core@2.3.18.Final//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at org.wildfly.extension.undertow@35.0.1.Final//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1421)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256)
        at io.undertow.servlet@2.3.18.Final//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101)
        at io.undertow.core@2.3.18.Final//io.undertow.server.Connectors.executeRootHandler(Connectors.java:395)
        at io.undertow.core@2.3.18.Final//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:861)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
        at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
        at org.jboss.xnio@3.8.16.Final//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
        at java.base/java.lang.Thread.run(Thread.java:1583)

先ほどは成功しおいた、ServletRequest#startAsyncの呌び出しが倱敗するようになりたす。

ちなみに、Filter自䜓は動いおいたす。

11:30:29,143 INFO  [org.littlewings.servlet.async.SimpleFilter] (default task-1) do filter@SimpleFilter

Apache Tomcat。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleFilter - do filter@SimpleFilter
[INFO] 2月 24, 2025 11:32:47 午前 org.apache.catalina.connector.Request startAsync
[INFO] 譊告: 凊理チェヌン内の次のクラスが非同期をサポヌトしおいないため、非同期を開始できたせん [org.littlewings.servlet.async.SimpleFilter]
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleAsyncServlet.doGet(SimpleAsyncServlet.java:24)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.littlewings.servlet.async.SimpleFilter.doFilter(SimpleFilter.java:23)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]
[INFO] 2月 24, 2025 11:32:47 午前 org.apache.catalina.core.StandardWrapperValve invoke
[INFO] 重倧: サヌブレット [org.littlewings.servlet.async.SimpleAsyncServlet] のServlet.service()が䟋倖を投げたした
[INFO] java.lang.IllegalStateException: 珟圚のチェヌンのフィルタたたはサヌブレットは非同期操䜜をサポヌトしおいたせん。
[INFO]  at org.apache.catalina.connector.Request.startAsync(Request.java:1510)
[INFO]  at org.apache.catalina.connector.RequestFacade.startAsync(RequestFacade.java:720)
[INFO]  at org.littlewings.servlet.async.SimpleAsyncServlet.doGet(SimpleAsyncServlet.java:24)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)
[INFO]  at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.littlewings.servlet.async.SimpleFilter.doFilter(SimpleFilter.java:23)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
[INFO]  at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
[INFO]  at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
[INFO]  at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
[INFO]  at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
[INFO]  at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
[INFO]  at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
[INFO]  at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:663)
[INFO]  at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
[INFO]  at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
[INFO]  at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
[INFO]  at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
[INFO]  at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905)
[INFO]  at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
[INFO]  at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190)
[INFO]  at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
[INFO]  at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
[INFO]  at java.base/java.lang.Thread.run(Thread.java:1583)
[INFO]

こちらも結果は同じです。

䞡方ずもHTTPステヌタスコヌド500になりたす。

ずいうわけで、このFilterはここで無効にしおおきたす。

// asyncSupportedがないFilterは実行できないServletRequest#startAsyncの呌び出しが倱敗する
// @WebFilter(urlPatterns = "/*")
public class SimpleFilter implements Filter {
asyncSupportedを有効にしたFilterを䜿う

では、次は@WebFilterのasyncSupportedをtrueにしたFilterを適甚しおみたす。

src/main/java/org/littlewings/servlet/async/SimpleAsyncFilter.java

package org.littlewings.servlet.async;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class SimpleAsyncFilter implements Filter {
    private Logger logger = LoggerFactory.getLogger(SimpleAsyncFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        logger.info("do filter@{}", getClass().getSimpleName());

        chain.doFilter(request, response);
    }
}

確認。

$ curl localhost:8080/async/simple

WildFly。

ログ。

11:35:54,639 INFO  [org.littlewings.servlet.async.SimpleAsyncFilter] (default task-1) do filter@SimpleAsyncFilter
11:35:54,640 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-1) [default task-1] dispatch async
11:35:55,641 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async1
11:35:56,644 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async2
11:35:57,646 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async3
11:35:58,647 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async4
11:35:59,649 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] in async5
11:36:00,651 INFO  [org.littlewings.servlet.async.SimpleAsyncServlet] (default task-2) [default task-2] complete async

Filterが動䜜した埌に、非同期凊理も動くようになりたした。

レスポンスはなにも倉わらないので省略したす。

Apache Tomcat。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncFilter - do filter@SimpleAsyncFilter
[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-3] dispatch async
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async1
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async2
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async3
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async4
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] in async5
[INFO] [http-nio-8080-exec-4] INFO org.littlewings.servlet.async.SimpleAsyncServlet - [http-nio-8080-exec-4] complete async

こちらもOKですね。

このFilterは適甚したたたにしたす。

スレッドプヌルを䜿っお非同期凊理を行う

最埌は、スレッドプヌルを䜿いたす。先ほどは非同期凊理を䜿っおみたものの、AsyncContext#startではリク゚ストを扱う
スレッドず同じスレッドプヌルのものが䜿われおいそうだずいう話でした。

今回はこのように倉曎。

src/main/java/org/littlewings/servlet/async/SimpleAsyncUseThreadServlet.java

package org.littlewings.servlet.async;

import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import jakarta.annotation.PostConstruct;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@WebServlet(urlPatterns = "/async/thread", asyncSupported = true)
public class SimpleAsyncUseThreadServlet extends HttpServlet {
    private Logger logger = LoggerFactory.getLogger(SimpleAsyncUseThreadServlet.class);

    private ExecutorService executorService;

    @PostConstruct
    void postConstruct() {
        try {
            // Jakarta ConcurrencyWildFly
            executorService = InitialContext.doLookup("java:comp/DefaultManagedExecutorService");
        } catch (NamingException e) {
            // Tomcat
            executorService = Executors.newFixedThreadPool(10);
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        AsyncContext asyncContext = request.startAsync(request, response);

        executorService.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

        logger.info("[{}] dispatch async", Thread.currentThread().getName());
        // HttpServletResponseを操䜜しおいるので良くない
        response.getWriter().printf(
                "%s [%s] dispatch async%n",
                LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                Thread.currentThread().getName()
        );
        response.getWriter().flush();
    }
}

スレッドプヌルは、WildFlyの堎合はJakarta ConcurrentyのManagedExecutorService、Apache Tomcatの堎合は
Executors#newFixedThreadPoolを䜿うこずにしたす。

    private ExecutorService executorService;

    @PostConstruct
    void postConstruct() {
        try {
            // Jakarta ConcurrencyWildFly
            executorService = InitialContext.doLookup("java:comp/DefaultManagedExecutorService");
        } catch (NamingException e) {
            // Tomcat
            executorService = Executors.newFixedThreadPool(10);
        }
    }

こういうコヌドだず、WildFly Glowではレむダヌを怜出できないのでee-concurrencyレむダヌを明瀺的に远加しおいたす。

                        <configuration>
                            <overwrite-provisioned-server>true</overwrite-provisioned-server>
                            <discover-provisioning-info>
                                <version>35.0.1.Final</version>
                                <layers-for-jndi>
                                    <layer>ee-concurrency</layer>
                                </layers-for-jndi>
                            </discover-provisioning-info>
                        </configuration>

倉曎点は、AsyncContext#startは䜿わずにExecutorService#executeに任せるようにしただけです。

        executorService.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);

                HttpServletResponse res = (HttpServletResponse) asyncContext.getResponse();

                PrintWriter writer = res.getWriter();

                for (int i = 0; i < 5; i++) {
                    int counter = i + 1;
                    logger.info("[{}] in async{}", Thread.currentThread().getName(), counter);
                    writer.printf(
                            "%s [%s] in async%d%n",
                            LocalDateTime.now().format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")),
                            Thread.currentThread().getName(),
                            counter
                    );
                    writer.flush();

                    TimeUnit.SECONDS.sleep(1L);
                }
            } catch (IOException | InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                logger.info("[{}] complete async", Thread.currentThread().getName());
                asyncContext.complete();
            }
        });

確認しおみたす。

$ curl localhost:8080/async/thread

WildFly。

レスポンス。

2025-02-24 11:44:12 [default task-1] dispatch async
2025-02-24 11:44:13 [EE-ManagedExecutorService-default-Thread-1] in async1
2025-02-24 11:44:14 [EE-ManagedExecutorService-default-Thread-1] in async2
2025-02-24 11:44:15 [EE-ManagedExecutorService-default-Thread-1] in async3
2025-02-24 11:44:16 [EE-ManagedExecutorService-default-Thread-1] in async4
2025-02-24 11:44:17 [EE-ManagedExecutorService-default-Thread-1] in async5

ログ。

11:44:12,691 INFO  [org.littlewings.servlet.async.SimpleAsyncFilter] (default task-1) do filter@SimpleAsyncFilter
11:44:12,695 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (default task-1) [default task-1] dispatch async
11:44:13,697 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async1
11:44:14,699 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async2
11:44:15,701 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async3
11:44:16,703 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async4
11:44:17,706 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] in async5
11:44:18,707 INFO  [org.littlewings.servlet.async.SimpleAsyncUseThreadServlet] (EE-ManagedExecutorService-default-Thread-1) [EE-ManagedExecutorService-default-Thread-1] complete async

圓然ずいえば圓然ですが、䜿われるスレッドが倉わりたした。

Apache Tomcat。

レスポンス。

2025-02-24 11:45:41 [http-nio-8080-exec-3] dispatch async
2025-02-24 11:45:42 [pool-1-thread-1] in async1
2025-02-24 11:45:43 [pool-1-thread-1] in async2
2025-02-24 11:45:44 [pool-1-thread-1] in async3
2025-02-24 11:45:45 [pool-1-thread-1] in async4
2025-02-24 11:45:46 [pool-1-thread-1] in async5

ログ。

[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncFilter - do filter@SimpleAsyncFilter
[INFO] [http-nio-8080-exec-3] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [http-nio-8080-exec-3] dispatch async
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async1
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async2
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async3
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async4
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] in async5
[INFO] [pool-1-thread-1] INFO org.littlewings.servlet.async.SimpleAsyncUseThreadServlet - [pool-1-thread-1] complete async

こちらも同じです。

今回の確認範囲はここたでにしたす。

AsyncContext#startで䜿われるスレッドの実䜓は

ずころで、AsyncContext#startではServletのリク゚ストを凊理するのず同じ皮類のスレッドが䜿われおいるように芋えたした。
本圓にそうなのか、コヌドで確認しおみたいず思いたす。

Undertowの堎合

Undertowの堎合は、このあたりに実装されおいたす。

以䞋の順で遞ぶようです。

  • Deploymentに非同期甚のExecutorが蚭定されおいればそれを䜿う
  • DeploymentにExecutorが蚭定されおいればそれを䜿う
  • ワヌカヌスレッドServletのリク゚ストを凊理するのず同じスレッドプヌルを䜿う

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L283-L293

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L295-L304

で、WildFlyで芋たずころワヌカヌスレッドが䜿われおいたした。

ちなみに、スレッドを自分で扱った堎合でもAsyncContext#completeを呌び出した時には内郚的にこの凊理が呌び出される
こずになり、この時はワヌカヌスレッドが䜿われるこずになりたす。

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L247

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L273-L278

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L431-L437

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L539

https://github.com/undertow-io/undertow/blob/2.3.18.Final/servlet/src/main/java/io/undertow/servlet/spec/AsyncContextImpl.java#L520

ディスパッチを行っおいるかどうかで挙動が違いそうなので、ディスパッチを行うパタヌンはたた確認したいですね。

Apache Tomcatの堎合

Apache TomcatでのAsyncContext#startの実装を芋おみたしょう。このあたりですね。

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/catalina/core/AsyncContextImpl.java#L234

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/coyote/AbstractProcessor.java#L559-L561

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/coyote/AsyncStateMachine.java#L475

ここでのAbstractProcessorの実装はHttp11ProcessorやAjpProcessorなどを指したす。

぀たり、Connectorず同じスレッドがやっぱり䜿われるわけですね。

AsyncContext#completeを呌び出した時は、呌び出し元のスレッドず同じものが䜿われおいたした。

このあたりが実装されおいるAsyncStateMachineには、以䞋のように非同期凊理の状態遷移に関するコメントがしっかりず
曞いおありたした。

/**
 * Manages the state transitions for async requests.
 *
 * <pre>
 * The internal states that are used are:
 * DISPATCHED       - Standard request. Not in Async mode.
 * STARTING         - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but service() has not exited.
 * STARTED          - ServletRequest.startAsync() has been called from
 *                    Servlet.service() and service() has exited.
 * READ_WRITE_OP    - Performing an asynchronous read or write.
 * MUST_COMPLETE    - ServletRequest.startAsync() followed by complete() have
 *                    been called during a single Servlet.service() method. The
 *                    complete() will be processed as soon as Servlet.service()
 *                    exits.
 * COMPLETE_PENDING - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but, before service() exited, complete()
 *                    was called from another thread. The complete() will
 *                    be processed as soon as Servlet.service() exits.
 * COMPLETING       - The call to complete() was made once the request was in
 *                    the STARTED state.
 * TIMING_OUT       - The async request has timed out and is waiting for a call
 *                    to complete() or dispatch(). If that isn't made, the error
 *                    state will be entered.
 * MUST_DISPATCH    - ServletRequest.startAsync() followed by dispatch() have
 *                    been called during a single Servlet.service() method. The
 *                    dispatch() will be processed as soon as Servlet.service()
 *                    exits.
 * DISPATCH_PENDING - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but, before service() exited, dispatch()
 *                    was called from another thread. The dispatch() will
 *                    be processed as soon as Servlet.service() exits.
 * DISPATCHING      - The dispatch is being processed.
 * MUST_ERROR       - ServletRequest.startAsync() has been called from
 *                    Servlet.service() but, before service() exited, an I/O
 *                    error occurred on another thread. The container will
 *                    perform the necessary error handling when
 *                    Servlet.service() exits.
 * ERROR            - Something went wrong.
 *
 *
 * The valid state transitions are:
 *
 *                  post()                                        dispatched()
 *    |-------»------------------»---------|    |-------«-----------------------«-----|
 *    |                                    |    |                                     |
 *    |                                    |    |        post()                       |
 *    |               post()              \|/  \|/       dispatched()                 |
 *    |           |-----»----------------»DISPATCHED«-------------«-------------|     |
 *    |           |                          | /|\ |                            |     |
 *    |           |              startAsync()|  |--|timeout()                   |     |
 *    ^           |                          |                                  |     |
 *    |           |        complete()        |                  dispatch()      ^     |
 *    |           |   |--«---------------«-- | ---«--MUST_ERROR--»-----|        |     |
 *    |           |   |                      |         /|\             |        |     |
 *    |           ^   |                      |          |              |        |     |
 *    |           |   |                      |    /-----|error()       |        |     |
 *    |           |   |                      |   /                     |        ^     |
 *    |           |  \|/  ST-complete()     \|/ /   ST-dispatch()     \|/       |     |
 *    |    MUST_COMPLETE«--------«--------STARTING--------»---------»MUST_DISPATCH    |
 *    |                                    / | \                                      |
 *    |                                   /  |  \                                     |
 *    |                    OT-complete() /   |   \    OT-dispatch()                   |
 *    |   COMPLETE_PENDING«------«------/    |    \-------»---------»DISPATCH_PENDING |
 *    |        |      /|\                    |                       /|\ |            |
 *    |        |       |                     |                        |  |post()      |
 *    |        |       |OT-complete()        |           OT-dispatch()|  |            |
 *    |        |       |---------«-------«---|---«--\                 |  |            |
 *    |        |                             |       \                |  |            |
 *    |        |         /-------«-------«-- | --«---READ_WRITE--»----|  |            |
 *    |        |        / ST-complete()      |        /  /|\  \          |            |
 *    |        |       /                     | post()/   /     \         |            |
 *    |        |      /                      |      /   /       \        |            |
 *    |        |     /                       |     /   /         \       |            |
 *    |        |    /                        |    /   /           \      |            |
 *    |        |   /                         |   |   /             \     |            |
 *    |        |  /                          |   |  /  ST-dispatch()\    |            |
 *    |        |  |                          |   | |                 \   |            |
 *    |  post()|  |  timeout()         post()|   | |asyncOperation()  \  |  timeout() |
 *    |        |  |  |--|                    |   | |                  |  |    |--|    |
 *    |       \|/\|/\|/ |     complete()    \|/ \|/|   dispatch()    \|/\|/  \|/ |    |
 *    |--«-----COMPLETING«--------«----------STARTED--------»---------»DISPATCHING----|
 *            /|\  /|\                       | /|\ |                       /|\ /|\
 *             |    |                        |  |--|                        |   |
 *             |    |               timeout()|  post()                      |   |
 *             |    |                        |                              |   |
 *             |    |       complete()      \|/         dispatch()          |   |
 *             |    |------------«-------TIMING_OUT--------»----------------|   |
 *             |                                                                |
 *             |            complete()                     dispatch()           |
 *             |---------------«-----------ERROR--------------»-----------------|
 *
 *
 * Notes: * For clarity, the transitions to ERROR which are valid from every state apart from
 *          STARTING are not shown.
 *        * All transitions may happen on either the Servlet.service() thread (ST) or on any
 *          other thread (OT) unless explicitly marked.
 * </pre>
 */
class AsyncStateMachine {

https://github.com/apache/tomcat/blob/10.1.36/java/org/apache/coyote/AsyncStateMachine.java#L30-L129

おわりに

Jakarta Servletの非同期凊理をWildFly 35.0.1.Final.、Apache Tomcat 10.1.36で詊しおみたした。

ほが觊れたこずがなかったのず、Jakarta Servletの仕様曞もいろいろず読むこずになったので勉匷になりたした。

ディスパッチたわりは今回は芋れなかったので、たたの機䌚に芋おみようかなず思いたす。